Replace daily standups with a Python script and GitHub Actions workflow that automatically aggregates commit logs, open pull requests, and review activity into a summary posted to Slack each morning. For a ten-person team, this cuts standup time from 750 minutes per week to roughly 75 minutes of asynchronous reading—recovering ten hours monthly that developers spend writing code instead of talking about writing code.

Why GitHub Commit Summaries Work as Standup Replacements

GitHub commit logs already contain what developers did yesterday, what they’re doing today, and any blockers. The information exists in your version control system—you’re just not extracting it effectively.

Automated commit summaries capture:

This approach works especially well for teams with four to fifteen engineers where meeting overhead becomes noticeable but too much asynchronous communication creates notification fatigue.

Setting Up Your Commit Summary Script

Create a Python script that runs on a schedule and generates a summary report:

#!/usr/bin/env python3
"""Generate daily standup summary from GitHub activity."""

import subprocess
from datetime import datetime, timedelta
from pathlib import Path

def get_git_log(days_ago=1):
    """Get commits from the last N days."""
    since = (datetime.now() - timedelta(days=days_ago)).strftime('%Y-%m-%d')
    result = subprocess.run(
        ['git', 'log', '--since', since, '--pretty=format:%h | %an | %s'],
        capture_output=True, text=True
    )
    return result.stdout.strip().split('\n') if result.stdout else []

def get_open_prs():
    """Get open pull requests."""
    result = subprocess.run(
        ['gh', 'pr', 'list', '--state', 'open', '--limit', '10', '--json', 'title,author,url'],
        capture_output=True, text=True
    )
    return result.stdout

def generate_summary():
    """Generate the daily standup summary."""
    commits = get_git_log()
    
    summary = f"# Standup Summary - {datetime.now().strftime('%Y-%m-%d')}\n\n"
    summary += "## Completed Work (Yesterday)\n\n"
    
    for commit in commits:
        if commit:
            parts = commit.split(' | ')
            summary += f"- `{parts[0]}` {parts[2]} ({parts[1]})\n"
    
    summary += "\n## Work In Progress\n\n"
    summary += "Run `gh pr list` to see open pull requests.\n"
    
    summary += "\n## Today's Plan\n\n"
    summary += "_Team members fill this in manually or via daily planning comment_\n"
    
    return summary

if __name__ == '__main__':
    print(generate_summary())

Save this as scripts/daily-standup.py and run it each morning with a cron job or GitHub Actions scheduled workflow.

Automating with GitHub Actions

Create a scheduled workflow that generates and shares the summary automatically:

name: Daily Standup Summary
on:
  schedule:
    - cron: '0 15 * * 1-5'  # 3 PM UTC, Mon-Fri
  workflow_dispatch:

jobs:
  generate-summary:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Generate commit summary
        run: |
          python scripts/daily-standup.py > standup-summary.md
      
      - name: Post to Slack
        uses: 8398a7/action-slack@v3
        with:
          status: custom
          fields: repo,message
          custom_payload: |
            {
              blocks: [
                {
                  type: "header",
                  text: { type: "plain_text", text: "Daily Standup Summary" }
                },
                {
                  type: "section",
                  text: { type: "mrkdwn", text: "Generated from GitHub activity" }
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

This workflow runs automatically and posts results to your Slack channel. Team members check the summary asynchronously rather than attending a live meeting.

Adding Blocker Detection

Blockers often appear as PRs stuck in review, failing builds, or merge conflicts. Add detection logic:

def get_blockers():
    """Identify potential blockers."""
    blockers = []
    
    # PRs with no activity for 2+ days
    result = subprocess.run(
        ['gh', 'pr', 'list', '--state', 'open', '--json', 'title,updatedAt,reviews'],
        capture_output=True, text=True
    )
    import json
    prs = json.loads(result.stdout)
    
    for pr in prs:
        updated = datetime.fromisoformat(pr['updatedAt'].replace('Z', '+00:00'))
        hours_stale = (datetime.now() - updated.replace(tzinfo=None)).total_seconds() / 3600
        
        if hours_stale > 48 and not pr.get('reviews'):
            blockers.append(f"- {pr['title']} (no reviews, {hours_stale:.0f}h old)")
    
    return blockers

def generate_summary_with_blockers():
    """Generate summary including blockers."""
    summary = generate_summary()
    blockers = get_blockers()
    
    if blockers:
        summary += "\n## 🚧 Blockers\n\n"
        summary += "\n".join(blockers)
    
    return summary

This highlights PRs that have been open for more than forty-eight hours without any review activity—clear indicators that something is blocked.

Making It Work for Your Team

Effective commit summary standups require consistent commit messages. Establish a team convention:

<type>: <short description>

- What changed
- Why it matters
- How to test (optional)

When developers write meaningful commit messages, the summary provides genuine value. Poor commit messages produce useless summaries regardless of your automation.

Some teams add a brief morning text update after reviewing the commit summary. Keep it to one sentence: “Working on X, no blockers” posted in Slack. This takes seconds and adds context the commit log cannot capture.

Calculating Time Savings

For a ten-person engineering team replacing fifteen-minute daily standups:

That’s ten hours recovered each month—time developers spend actually writing code rather than talking about writing code.


Built by theluckystrike — More at zovo.one