Skip to content

Daily Summary

Daily Summary #122

Workflow file for this run

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"