Daily Summary #122
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Daily Summary | |
| on: | |
| schedule: | |
| - cron: "0 5 * * *" # 07:00 CEST (Stockholm) | |
| workflow_dispatch: # manual trigger for testing | |
| jobs: | |
| post-summary: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Gather project data | |
| id: data | |
| uses: actions/github-script@v8 | |
| with: | |
| github-token: ${{ secrets.PROJECT_TOKEN }} | |
| result-encoding: string | |
| script: | | |
| const query = `query { | |
| organization(login: "II1302-8") { | |
| projectV2(number: 1) { | |
| items(first: 100) { | |
| nodes { | |
| fieldValues(first: 10) { | |
| nodes { | |
| ... on ProjectV2ItemFieldSingleSelectValue { | |
| name | |
| field { ... on ProjectV2SingleSelectField { name } } | |
| } | |
| ... on ProjectV2ItemFieldIterationValue { | |
| title | |
| startDate | |
| duration | |
| field { ... on ProjectV2IterationField { name } } | |
| } | |
| } | |
| } | |
| content { | |
| ... on Issue { | |
| title | |
| url | |
| repository { name } | |
| labels(first: 10) { nodes { name } } | |
| } | |
| ... on PullRequest { | |
| title | |
| url | |
| repository { name } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }`; | |
| const result = await github.graphql(query); | |
| const items = result.organization.projectV2.items.nodes; | |
| const today = new Date(); | |
| today.setHours(0, 0, 0, 0); | |
| const parsed = items.map(item => { | |
| const fields = {}; | |
| let sprintStart = null; | |
| let sprintEnd = null; | |
| for (const fv of item.fieldValues.nodes) { | |
| if (fv.field) { | |
| fields[fv.field.name] = fv.name || fv.title; | |
| if (fv.field.name === 'Sprint' && fv.startDate) { | |
| sprintStart = new Date(fv.startDate); | |
| sprintEnd = new Date(fv.startDate); | |
| sprintEnd.setDate(sprintEnd.getDate() + fv.duration); | |
| } | |
| } | |
| } | |
| return { | |
| ...item.content, | |
| status: fields['Status'], | |
| sprint: fields['Sprint'], | |
| sprintStart, | |
| sprintEnd | |
| }; | |
| }).filter(item => item.title); | |
| const currentSprintItem = parsed.find(item => | |
| item.sprintStart && item.sprintStart <= today && today <= item.sprintEnd | |
| ); | |
| if (!currentSprintItem) { | |
| core.setFailed('No active sprint found for today'); | |
| return; | |
| } | |
| const currentSprint = currentSprintItem.sprint; | |
| const daysLeft = Math.ceil( | |
| (currentSprintItem.sprintEnd - today) / (1000 * 60 * 60 * 24) | |
| ); | |
| const sprintItems = parsed.filter(i => i.sprint === currentSprint); | |
| const counts = { 'Todo': 0, 'In Progress': 0, 'In Review': 0, 'Done': 0 }; | |
| for (const item of sprintItems) { | |
| if (item.status in counts) counts[item.status]++; | |
| } | |
| const total = sprintItems.length; | |
| const doneCount = counts['Done']; | |
| const percent = total > 0 ? Math.round((doneCount / total) * 100) : 0; | |
| const prs = []; | |
| for (const repo of ['dockpulse', 'dockpulse-iot']) { | |
| const { data } = await github.rest.pulls.list({ | |
| owner: 'II1302-8', | |
| repo, | |
| state: 'open' | |
| }); | |
| for (const pr of data) { | |
| if (!pr.draft) { | |
| prs.push({ | |
| repo, | |
| number: pr.number, | |
| title: pr.title, | |
| url: pr.html_url | |
| }); | |
| } | |
| } | |
| } | |
| const prText = prs.length > 0 | |
| ? prs.map(p => `[${p.repo}#${p.number}](${p.url}) -- ${p.title}`).join('\n') | |
| : 'None'; | |
| const blocked = sprintItems.filter(i => | |
| i.labels?.nodes?.some(l => l.name === 'blocked') | |
| ); | |
| const blockedText = blocked.length > 0 | |
| ? blocked.map(i => `[${i.title}](${i.url})`).join('\n') | |
| : 'None'; | |
| const dockpulseCount = sprintItems.filter(i => i.repository?.name === 'dockpulse').length; | |
| const iotCount = sprintItems.filter(i => i.repository?.name === 'dockpulse-iot').length; | |
| // Format date for title | |
| const dateStr = today.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); | |
| // Write data for subsequent steps | |
| const fs = require('fs'); | |
| const data = { | |
| currentSprint, percent, doneCount, total, daysLeft, | |
| counts, blocked: blocked.length, prText, blockedText, | |
| dockpulseCount, iotCount, dateStr, totalIssues: total | |
| }; | |
| fs.writeFileSync('/tmp/summary.json', JSON.stringify(data)); | |
| return String(percent); | |
| - name: Generate progress ring | |
| run: | | |
| node scripts/progress-ring.mjs ${{ steps.data.outputs.result }} /tmp/ring.svg | |
| sudo apt-get install -y -qq librsvg2-bin > /dev/null 2>&1 | |
| rsvg-convert /tmp/ring.svg -o /tmp/ring.png | |
| - name: Post to Discord | |
| env: | |
| DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} | |
| run: | | |
| DATA=$(cat /tmp/summary.json) | |
| SPRINT=$(echo "$DATA" | jq -r '.currentSprint') | |
| DONE=$(echo "$DATA" | jq -r '.counts.Done') | |
| REVIEW=$(echo "$DATA" | jq -r '.counts["In Review"]') | |
| PROGRESS=$(echo "$DATA" | jq -r '.counts["In Progress"]') | |
| TODO=$(echo "$DATA" | jq -r '.counts.Todo') | |
| BLOCKED=$(echo "$DATA" | jq -r '.blocked') | |
| DAYS_LEFT=$(echo "$DATA" | jq -r '.daysLeft') | |
| PR_TEXT=$(echo "$DATA" | jq -r '.prText') | |
| BLOCKED_TEXT=$(echo "$DATA" | jq -r '.blockedText') | |
| DOCKPULSE=$(echo "$DATA" | jq -r '.dockpulseCount') | |
| IOT=$(echo "$DATA" | jq -r '.iotCount') | |
| DATE_STR=$(echo "$DATA" | jq -r '.dateStr') | |
| # Build fields array conditionally | |
| FIELDS=$(jq -n \ | |
| --arg done "$DONE" \ | |
| --arg review "$REVIEW" \ | |
| --arg progress "$PROGRESS" \ | |
| --arg todo "$TODO" \ | |
| --arg blocked "$BLOCKED" \ | |
| --arg days "$DAYS_LEFT" \ | |
| --arg prs "$PR_TEXT" \ | |
| --arg blockers "$BLOCKED_TEXT" \ | |
| --arg dp "$DOCKPULSE" \ | |
| --arg iot "$IOT" \ | |
| '[ | |
| { name: "Done", value: $done, inline: true }, | |
| { name: "In Review", value: $review, inline: true }, | |
| { name: "In Progress", value: $progress, inline: true }, | |
| { name: "Todo", value: $todo, inline: true }, | |
| { name: "Blocked", value: $blocked, inline: true }, | |
| { name: "Days Left", value: $days, inline: true }, | |
| { name: "dockpulse", value: ("[" + $dp + " issues](https://github.com/II1302-8/dockpulse/issues)"), inline: true }, | |
| { name: "dockpulse-iot", value: ("[" + $iot + " issues](https://github.com/II1302-8/dockpulse-iot/issues)"), inline: true }, | |
| { name: "\u200b", value: "\u200b", inline: true }, | |
| { name: "PRs Awaiting Review", value: $prs, inline: false } | |
| ] | |
| + if $blockers != "None" then | |
| [{ name: "Blockers", value: $blockers, inline: false }] | |
| else [] end') | |
| TOTAL=$(echo "$DATA" | jq -r '.totalIssues') | |
| PERCENT=$(echo "$DATA" | jq -r '.percent') | |
| PAYLOAD=$(jq -n \ | |
| --arg sprint "$SPRINT" \ | |
| --arg date "$DATE_STR" \ | |
| --arg footer "$TOTAL issues total | $PERCENT% complete" \ | |
| --argjson fields "$FIELDS" \ | |
| '{ | |
| embeds: [{ | |
| title: ("DockPulse \u2014 " + $sprint + " | " + $date), | |
| url: "https://github.com/orgs/II1302-8/projects/1", | |
| color: 3447003, | |
| thumbnail: { url: "attachment://ring.png" }, | |
| fields: $fields, | |
| footer: { text: $footer } | |
| }] | |
| }') | |
| curl -s -X POST \ | |
| -F "payload_json=$PAYLOAD" \ | |
| -F "file=@/tmp/ring.png" \ | |
| "$DISCORD_WEBHOOK_URL" |