fx Scheduling & CI

Weekly report automation with GitHub Actions

⏱ 15 min

What you'll learn

  • Why GitHub Actions?
  • Repository layout
  • The workflow file

Concept

1. Why GitHub Actions?

  • Free cloud computers that run your script on a schedule.
  • Your PC can be off.
  • Every run is logged, and failures send you an email.
  • Works for pandas/openpyxl scripts (no Excel needed). Not for VBA or xlwings.

2. Repository layout

monthly-report/                ← a GitHub repository (make it PRIVATE for business data)
    .github/workflows/weekly-report.yml
    sample-data/*.csv
    monthly_report.py
    send_mail.py
    requirements.txt

requirements.txt:

pandas
openpyxl

Pin versions (e.g. pandas==2.2.3) once it works, so a future library update doesn't break your report.

3. The workflow file

Create .github/workflows/weekly-report.yml:

name: Weekly sales report

on:
  schedule:
    - cron: "30 3 * * 1"        # every Monday 03:30 UTC = 09:00 IST
  workflow_dispatch:            # adds a "Run workflow" button for manual runs

permissions:
  contents: read

jobs:
  report:
    runs-on: ubuntu-latest
    env:
      SMTP_USER: ${{ secrets.SMTP_USER }}
      SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
      REPORT_TO: ${{ secrets.REPORT_TO }}
    steps:
      - name: Get the code and data
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: pip

      - name: Install libraries
        run: pip install -r requirements.txt

      - name: Build the report
        run: python monthly_report.py --input sample-data --output Sales_Report.xlsx

      - name: Save report as a downloadable artifact
        uses: actions/upload-artifact@v4
        with:
          name: sales-report
          path: Sales_Report.xlsx
          retention-days: 30

      - name: Email the report
        if: ${{ env.SMTP_USER != '' && env.SMTP_PASSWORD != '' && env.REPORT_TO != '' }}
        env:
          SMTP_HOST: smtp.gmail.com
          SMTP_PORT: "587"
          SMTP_USER: ${{ secrets.SMTP_USER }}
          SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
        run: python send_mail.py Sales_Report.xlsx "$REPORT_TO"

Commit and push. Open the Actions tab → Weekly sales report → Run workflow to test right away.

4. Cron is in UTC

GitHub schedules use UTC. India is UTC + 5:30, so subtract 5 h 30 min:

Want (IST) Write (UTC)
Mon 09:00 30 3 * * 1
Daily 08:00 30 2 * * *
Daily 18:30 0 13 * * *
1st of month 07:00 30 1 1 * *

Scheduled runs can start a few minutes late (sometimes more at busy times). Don't rely on exact minutes.

5. The email script

send_mail.py (also used in the capstone) reads all settings from environment variables:

"""
send_mail.py
Email a file as an attachment through SMTP.
Settings are read from environment variables, never written in the code:
    SMTP_HOST (default smtp.gmail.com), SMTP_PORT (default 587), SMTP_USER, SMTP_PASSWORD

Usage:
    python send_mail.py Sales_Report.xlsx owner@example.com
"""
import mimetypes
import os
import smtplib
import sys
from email.message import EmailMessage
from pathlib import Path


def send_file(path: Path, to: str, subject: str, body: str) -> None:
    user = os.environ["SMTP_USER"]
    msg = EmailMessage()
    msg["From"] = user
    msg["To"] = to
    msg["Subject"] = subject
    msg.set_content(body)

    ctype, _ = mimetypes.guess_type(path.name)
    maintype, subtype = (ctype or "application/octet-stream").split("/", 1)
    msg.add_attachment(path.read_bytes(), maintype=maintype, subtype=subtype, filename=path.name)

    host = os.environ.get("SMTP_HOST", "smtp.gmail.com")
    port = int(os.environ.get("SMTP_PORT", "587"))
    with smtplib.SMTP(host, port, timeout=60) as server:
        server.starttls()
        server.login(user, os.environ["SMTP_PASSWORD"])
        server.send_message(msg)


if __name__ == "__main__":
    if len(sys.argv) != 3:
        sys.exit("Usage: python send_mail.py <file> <to-address>")
    file_path = Path(sys.argv[1])
    send_file(file_path, sys.argv[2], f"Report: {file_path.name}", "Hello,\n\nThe latest report is attached.\n")
    print("Email sent.")

6. Secrets — never put passwords in the code

Repository → Settings → Secrets and variables → Actions → New repository secret:

Name Value
SMTP_USER the sending Gmail address
SMTP_PASSWORD a Gmail App Password (Google Account → Security → 2-Step Verification must be on → App passwords), not your normal password
REPORT_TO who receives the report

Secrets are hidden in logs and can't be read back, even by you.

7. Where does the data come from?

In this example the CSVs are committed to the repo. Real options:

  • Someone commits the new files each week (simple).
  • The script downloads them: from an API (requests), Google Drive/Sheets API, or cloud storage — with credentials in Secrets.

Private data → private repository. Anything in a public repo is visible to the world.

8. Things to know

  • Scheduled workflows run only from the default branch (usually main).
  • In public repos, scheduled workflows are switched off after 60 days with no repository activity — GitHub emails you first; a commit or re-enabling turns them back on.
  • Private repos get a monthly quota of free Actions minutes (check your plan). A small report uses about a minute per run.
  • If a run fails, GitHub emails you, and the Actions tab shows the full log of each step.

Common mistakes

Writing the cron in IST. Committing customer data to a public repo. Using your normal Gmail password (blocked) instead of an App Password. Forgetting workflow_dispatch, so you have to wait a week to test.

Exercises

mediumCreate a private repository with your monthly_report script, sample CSVs, requirements.txt and the workflow. Run it manually and download the artifact. Then add the three secrets and confirm the email arrives. Finally, change the schedule to run daily at 08:00 IST.
Copy the bundled workflow to .github/workflows/weekly-report.yml in your practice repository; it builds from sample-data by default. Dispatch manually and inspect the sales-report artifact before configuring SMTP_USER, SMTP_PASSWORD and REPORT_TO secrets. Daily 08:00 IST is cron 30 2 * * * (UTC). Enable email only after confirming the recipient; scheduled workflows can be delayed.

Quiz

Cron for Monday 09:00 IST in GitHub Actions?
30 3 * * 1
Where should the SMTP password go?
GitHub Secrets
Which line adds a manual "Run workflow" button?
workflow_dispatch
Weekly report automation with GitHub Actions · Automation | ExcelWalaa