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.