diff --git a/.github/scripts/project-report/post-to-slack.js b/.github/scripts/project-report/post-to-slack.js deleted file mode 100644 index 58ca4b50..00000000 --- a/.github/scripts/project-report/post-to-slack.js +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Reads a report JSON from stdin and posts it to Slack as a Block Kit message. - * - * Usage: cat /tmp/report.json | node post-to-slack.js - * - * Expects WEEKLY_REPORT_SLACK_WEBHOOK_URL environment variable. - * No external dependencies — uses only Node built-ins. - */ - -import { readFileSync } from 'node:fs'; - -// --------------------------------------------------------------------------- -// Slack Block Kit helpers -// --------------------------------------------------------------------------- - -const REPO = 'videojs/v10'; -const BOARD_URL = 'https://github.com/orgs/videojs/projects/7/views/2'; - -/** Round SP values to avoid floating point artifacts like 303.669999... */ -function sp(value) { - return Math.round(value); -} - -function pct(value, total) { - if (total === 0) return '0%'; - return `${Math.round((value / total) * 100)}%`; -} - -function statusEmoji(status) { - if (status === 'Done') return '🟢'; - if (status === 'Blocked') return '🔴'; - if (['In progress', 'Up next', 'Ready for review'].includes(status)) return '🔵'; - return '⚪'; -} - -function header(text) { - return { type: 'header', text: { type: 'plain_text', text, emoji: true } }; -} - -function section(text) { - return { type: 'section', text: { type: 'mrkdwn', text } }; -} - -function divider() { - return { type: 'divider' }; -} - -function context(text) { - return { type: 'context', elements: [{ type: 'mrkdwn', text }] }; -} - -// --------------------------------------------------------------------------- -// Build Slack blocks from report JSON -// --------------------------------------------------------------------------- - -function buildBlocks(report) { - const blocks = []; - const { velocity: v, beta_progress: bp, without_spf: ns } = report; - - // Header - blocks.push(header('Video.js 10 Weekly Report')); - blocks.push(context(`*${report.header}*`)); - blocks.push(divider()); - - // Velocity - blocks.push(section( - `*This week*\n` + - `• Story Points completed: *${sp(v.sp_completed)}*\n` + - `• Items completed: *${v.items_completed}*\n` + - `• PRs merged: *${v.prs_merged}*\n` + - `• Issues closed: *${v.issues_closed}*`, - )); - blocks.push(divider()); - - // Beta progress by status - const statusLines = bp.by_status - .sort((a, b) => b.sp - a.sp) - .map((s) => `${statusEmoji(s.status)} ${s.status}: *${sp(s.sp)} SP* (${pct(s.sp, bp.total_sp)}) — ${s.count} items`) - .join('\n'); - blocks.push(section(`*Beta progress* — ${bp.total_items} items, ${sp(bp.total_sp)} SP\n${statusLines}`)); - - // Without SPF - blocks.push(section( - `*Without SPF* — ${sp(ns.total_sp)} SP\n` + - `🟢 Done: *${sp(ns.done_sp)} SP* (${pct(ns.done_sp, ns.total_sp)})\n` + - `🔵 Active: *${sp(ns.active_sp)} SP* (${pct(ns.active_sp, ns.total_sp)})\n` + - `🔴 Blocked: *${sp(ns.blocked_sp)} SP* (${pct(ns.blocked_sp, ns.total_sp)})\n` + - `⚪ Unplanned: *${sp(ns.unplanned_sp)} SP* (${pct(ns.unplanned_sp, ns.total_sp)})`, - )); - blocks.push(divider()); - - // Workstreams - if (report.by_workstream.length > 0) { - const wsLines = report.by_workstream - .sort((a, b) => b.total - a.total) - .map((ws) => `• ${ws.name}: *${sp(ws.total)} SP* — ${pct(ws.done, ws.total)} done`) - .join('\n'); - blocks.push(section(`*By workstream*\n${wsLines}`)); - blocks.push(divider()); - } - - // Team breakdown - if (report.team_breakdown.length > 0) { - const teamLines = report.team_breakdown - .sort((a, b) => b.sp - a.sp) - .map((t) => `• @${t.assignee}: *${sp(t.sp)} SP* (${t.items} items, ${t.prs} PRs)`) - .join('\n'); - blocks.push(section(`*Team breakdown*\n${teamLines}`)); - blocks.push(divider()); - } - - // UC Epics - if (report.uc_epics.length > 0) { - const epicLines = report.uc_epics - .map((e) => `• ${e.id} ${e.name}: *${sp(e.done_sp)}/${sp(e.total_sp)} SP* (${pct(e.done_sp, e.total_sp)}) — ${e.owner}`) - .join('\n'); - blocks.push(section(`*Use case epics*\n${epicLines}`)); - blocks.push(divider()); - } - - // Blocked items - if (report.blocked_items.length > 0) { - const blockedLines = report.blocked_items - .map((i) => `• ${i.title}`) - .join('\n'); - blocks.push(section(`*Blocked*\n${blockedLines}`)); - blocks.push(divider()); - } - - // What's next (cap at 10 to stay within block limits) - if (report.whats_next.length > 0) { - const items = report.whats_next.slice(0, 10); - const lines = items - .map((i) => { - const assignees = (i.assignees || []).map((a) => `@${a}`).join(', ') || 'unassigned'; - return `${statusEmoji(i.status)} ${i.title} (${sp(i.sp)} SP, ${assignees})`; - }) - .join('\n'); - const overflow = report.whats_next.length > 10 - ? `\n_…and ${report.whats_next.length - 10} more_` - : ''; - blocks.push(section(`*What's next*\n${lines}${overflow}`)); - blocks.push(divider()); - } - - // AI summary - if (report.ai_summary) { - blocks.push(section(`*Analysis*\n${report.ai_summary}`)); - blocks.push(divider()); - } - - // Footer - blocks.push(context(`<${BOARD_URL}|View project board>`)); - - // Slack limit: 50 blocks max - if (blocks.length > 50) { - const truncated = blocks.slice(0, 49); - truncated.push(context('_Report truncated — too many sections._')); - return truncated; - } - - return blocks; -} - -// --------------------------------------------------------------------------- -// Post to Slack webhook -// --------------------------------------------------------------------------- - -async function post(webhookUrl, payload) { - const res = await fetch(webhookUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }); - - if (!res.ok) { - const body = await res.text(); - throw new Error(`Slack webhook failed (${res.status}): ${body}`); - } -} - -// --------------------------------------------------------------------------- -// Main -// --------------------------------------------------------------------------- - -const webhookUrl = process.env.WEEKLY_REPORT_SLACK_WEBHOOK_URL; -if (!webhookUrl) { - console.error('WEEKLY_REPORT_SLACK_WEBHOOK_URL is not set'); - process.exit(1); -} - -// Read JSON from stdin -const input = readFileSync('/dev/stdin', 'utf-8').trim(); - -let report; -try { - report = JSON.parse(input); -} catch (err) { - console.error('Failed to parse report JSON:', err.message); - console.error('Input (first 500 chars):', input.slice(0, 500)); - process.exit(1); -} - -const blocks = buildBlocks(report); -const fallback = `Video.js 10 Weekly Report — ${report.header}`; - -await post(webhookUrl, { text: fallback, blocks }); -console.log(`Posted to Slack: ${report.header}`); diff --git a/.github/scripts/project-report/prompt.md b/.github/scripts/project-report/prompt.md deleted file mode 100644 index ae84096c..00000000 --- a/.github/scripts/project-report/prompt.md +++ /dev/null @@ -1,174 +0,0 @@ -You are generating the weekly progress report for **Video.js 10**. - -## Step 1: Determine date range - -Calculate the most recent completed full week (Monday–Sunday). If today is -Monday, that means last Mon–Sun. If today is any other day, it is the -Monday–Sunday of the current or just-ended week. - -The display label should show the full week: "Feb 23 – Mar 1, 2026". For -GitHub search queries, use exactly that Monday–Sunday range. - -## Step 2: Collect data from GitHub - -Use `gh` CLI commands. Batch GraphQL queries with aliases to stay within -rate limits (5,000 points/hour). - -### Project board items - -Fetch ALL items with manual cursor pagination. **Do not use `--paginate`** — -it duplicates GraphQL results. Use this pattern: - -```bash -CURSOR="" -ALL_ITEMS="[]" -while true; do - if [ -z "$CURSOR" ]; then AFTER_ARG=""; else AFTER_ARG=", after: \"$CURSOR\""; fi - RESULT=$(gh api graphql -f query='{ node(id: "PVT_kwDOADIolc4BHP_1") { ... on ProjectV2 { items(first: 100'"$AFTER_ARG"') { pageInfo { hasNextPage endCursor } nodes { status: fieldValueByName(name: "Status") { ... on ProjectV2ItemFieldSingleSelectValue { name } } points: fieldValueByName(name: "Story Points") { ... on ProjectV2ItemFieldNumberValue { number } } content { ... on Issue { number title state milestone { title } labels(first: 10) { nodes { name } } assignees(first: 5) { nodes { login } } } } } } } } }') - ITEMS=$(echo "$RESULT" | jq '.data.node.items.nodes') - ALL_ITEMS=$(echo "$ALL_ITEMS $ITEMS" | jq -s '.[0] + .[1]') - HAS_NEXT=$(echo "$RESULT" | jq -r '.data.node.items.pageInfo.hasNextPage') - CURSOR=$(echo "$RESULT" | jq -r '.data.node.items.pageInfo.endCursor') - if [ "$HAS_NEXT" != "true" ]; then break; fi -done -echo "$ALL_ITEMS" > /tmp/project_items.json -``` - -### Merged PRs and closed issues - -```bash -gh pr list --repo videojs/v10 --state merged --search "merged:START..END" --limit 100 --json number,title,author -gh issue list --repo videojs/v10 --state closed --search "closed:START..END" --limit 100 --json number,title,closedAt -``` - -### UC epic sub-issues - -Fetch sub-issues via REST, then batch story points with aliased GraphQL: - -```bash -gh api repos/videojs/v10/issues/{NUMBER}/sub_issues --jq '[.[] | {number, title, state}]' -``` - -Known UC epics: - -| Epic | Issue | Owner | -|------|-------|-------| -| UC-1 Core Playback UI | #489 | Rahim | -| UC-2 Adaptive Streaming | #353 | Wes | -| UC-3 Skins | #490 | Sam | -| UC-4 Captions & Subtitles | #491 | Rahim | -| UC-5 Accessibility | #492 | Rahim | -| UC-8 Keyboard & Power-User | #494 | Rahim | - -Check for new epics each week: -```bash -gh issue list --repo videojs/v10 --label epic --milestone Beta --state all --json number,title -``` - -## Step 3: Calculate metrics - -Apply these rules to the collected data: - -- **Filter to Beta milestone only** -- **Exclude items labeled `epic`** — these are rollups that double-count SP -- **Status values are case-sensitive**: `Done`, `In progress`, `Up next`, `Ready for review`, `Blocked` -- **SPF dominates** (~66% of SP) — always compute a "Without SPF" breakdown - -### Workstream label mapping - -| Label | Workstream | -|-------|-----------| -| `spf` | SPF | -| `components` | UI Components | -| `skin` | Skins | -| `media` | Media | -| `a11y` | Accessibility | -| Any label starting with `docs` | Docs & Guides | -| `site` | Site | -| `compiler` | Compiler | -| `store`, `pkg:core`, `pkg:dom` | Core / Store | -| Everything else | Other | - -Compute: -1. **Velocity** — SP and items completed THIS WEEK (closed in the date range) -2. **Beta progress** — all-time by status (Done, Active, Blocked, Unplanned) -3. **Without SPF** — same breakdown excluding `spf`-labeled items -4. **By workstream** — total, done, active, blocked per workstream -5. **Team breakdown** — this week's completed SP/items/PRs per assignee -6. **UC epics** — done SP / total SP per epic -7. **Blocked items** — anything with status "Blocked" -8. **What's next** — open items with status In progress, Up next, Ready for review - -## Step 4: Post to Slack - -Write the report as a JSON file at `/tmp/report.json` matching this exact structure, -then run the Slack posting script. - -```json -{ - "header": "Feb 23 – Mar 1, 2026", - "velocity": { - "sp_completed": 42, - "items_completed": 8, - "prs_merged": 12, - "issues_closed": 8 - }, - "beta_progress": { - "total_items": 150, - "total_sp": 234, - "by_status": [ - { "status": "Done", "sp": 89, "count": 45 }, - { "status": "In progress", "sp": 30, "count": 12 } - ] - }, - "without_spf": { - "total_sp": 156, - "done_sp": 67, - "active_sp": 34, - "blocked_sp": 5, - "unplanned_sp": 50 - }, - "by_workstream": [ - { "name": "SPF", "total": 78, "done": 35, "active": 20, "blocked": 3 } - ], - "team_breakdown": [ - { "assignee": "rahim", "sp": 18, "items": 5, "prs": 4 } - ], - "uc_epics": [ - { "id": "UC-1", "name": "Core Playback UI", "owner": "Rahim", "done_sp": 10, "total_sp": 25 } - ], - "blocked_items": [ - { "number": 142, "title": "Waiting on external API docs" } - ], - "whats_next": [ - { "number": 200, "title": "Slider a11y audit", "status": "In progress", "assignees": ["rahim"], "sp": 5 } - ], - "ai_summary": "2-3 paragraph narrative. See writing guidelines below." -} -``` - -Then post it: - -```bash -cat /tmp/report.json | node .github/scripts/project-report/post-to-slack.js -``` - -## Writing style for ai_summary - -- Direct, confident, friendly but not chatty -- Active voice, short sentences -- No filler: "In order to", "basically", "simply", "just", "very", "actually" -- No hedging: "might", "could", "perhaps" -- Use "we" and "our" for the project team -- Reference issues as #NUMBER, people as @name -- Cover: what shipped, velocity trends, risks/blockers, what to focus on next - -## Gotchas - -- **Status values are case-sensitive in jq.** Verify with `[.[].status.name] | unique`. -- **`--paginate` duplicates GraphQL results.** Always use manual cursor pagination. -- **Write jq to a file.** Shell quoting of `//` (jq's alternative operator) breaks in - bash inline strings. Save to `/tmp/calc.jq` and use `jq -f`. -- **`fieldValueByName` needs aliases.** Can't appear twice in one GraphQL selection. - Use `status: fieldValueByName(name: "Status")` and - `points: fieldValueByName(name: "Story Points")`. diff --git a/.github/workflows/weekly-project-report.yml b/.github/workflows/weekly-project-report.yml deleted file mode 100644 index bc0f3c18..00000000 --- a/.github/workflows/weekly-project-report.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Weekly Project Report - -on: - schedule: - # Every Monday at 9 AM UTC - - cron: '0 9 * * 1' - workflow_dispatch: - # Manual trigger for testing - -permissions: - actions: read - contents: read - issues: read - pull-requests: read - id-token: write - -jobs: - report: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v5 - - - name: Generate and post weekly report - uses: anthropics/claude-code-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - github_token: ${{ secrets.GITHUB_TOKEN }} - claude_args: | - --model sonnet - --max-turns 25 - --allowedTools "Bash(gh:*)" "Bash(jq:*)" "Bash(node:*)" "Bash(cat:*)" "Bash(echo:*)" "Read" "Write" - prompt: | - Read the instructions in `.github/scripts/project-report/prompt.md` and follow them. - - The Slack posting script is at `.github/scripts/project-report/post-to-slack.js`. - The webhook URL is in the WEEKLY_REPORT_SLACK_WEBHOOK_URL environment variable. - Use GH_TOKEN for authenticated gh commands — it is already set. - - Generate the weekly report, write the JSON to /tmp/report.json, then post it to Slack. - env: - # PAT with read:project scope — required for org-level Projects v2 GraphQL. - # The automatic GITHUB_TOKEN does not have project board access. - GH_TOKEN: ${{ secrets.PROJECT_GH_TOKEN }} - WEEKLY_REPORT_SLACK_WEBHOOK_URL: ${{ secrets.WEEKLY_REPORT_SLACK_WEBHOOK_URL }} - - - name: Alert Slack on failure - if: failure() - env: - WEEKLY_REPORT_SLACK_WEBHOOK_URL: ${{ secrets.WEEKLY_REPORT_SLACK_WEBHOOK_URL }} - run: | - curl -sf -X POST "$WEEKLY_REPORT_SLACK_WEBHOOK_URL" \ - -H 'Content-type: application/json' \ - -d '{"text":"⚠️ Weekly project report workflow failed. Check GitHub Actions: https://github.com/videojs/v10/actions"}'