Part 4 of 4
LaLiga bot: automating with GitHub Actions
The cron workflow, repository secrets and local setup with .env so the bot runs on its own.
The whole system was built and working locally. What was missing was what makes it “serverless”: having GitHub run the script for me every X hours and pass it the credentials without exposing them.
The workflow
I created .github/workflows/sincronizar.yml. It has two triggers: a schedule with cron and a workflow_dispatch to run it by hand from the UI (essential for testing without waiting for the scheduled time).
name: Sincronización de Calendario y Alertas
on:
schedule:
- cron: '0 */4 * * *'
workflow_dispatch:
jobs:
run-bot:
runs-on: ubuntu-latest
steps:
- name: Configurar el Repositorio
uses: actions/checkout@v4
- name: Configurar Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Instalar Dependencias
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Ejecutar Bot de Sincronización
env:
GOOGLE_CALENDAR_ID: ${{ secrets.GOOGLE_CALENDAR_ID }}
GOOGLE_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_JSON }}
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
EQUIPO_OBJETIVO: ${{ secrets.EQUIPO_OBJETIVO }}
URL_LIV_DIVISION: ${{ secrets.URL_LIV_DIVISION }}
run: python main.py
The env block injects the repository secrets as environment variables on the runner. That’s why in main.py we read everything with os.environ.get(...).
Repository secrets
I created them under Settings → Secrets and variables → Actions in the repo:
| Secret | Description |
|---|---|
EQUIPO_OBJETIVO |
Exact team name as it appears on the El Mundo site. |
URL_LIV_DIVISION |
URL of the Primera or Segunda División calendar. |
GOOGLE_CALENDAR_ID |
Google calendar identifier. |
GOOGLE_SERVICE_ACCOUNT_JSON |
Full service account JSON. |
TELEGRAM_BOT_TOKEN |
Bot token from @BotFather. |
TELEGRAM_CHAT_ID |
Numeric ID of your Telegram chat. |
Cron and the UTC trap
GitHub Actions runs cron schedules in UTC, not Spanish time. And Spain changes time zone:
- During daylight saving (March–October) it’s UTC+2.
- During standard time (October–March) it’s UTC+1.
If you want the script to run at 8 and 20 in peninsular time during winter, the UTC cron must be '0 7,19 * * *'.
Lesson: GitHub cron jobs run in UTC and, on top of that, GitHub doesn’t guarantee precise timing: an hourly job can be delayed 15–30 minutes depending on load. Also, don’t abuse high frequencies (for example every 5 minutes), because shared-use policies can block your repository’s Actions. For a sports calendar, every 4 hours is more than enough.
Local development with .env
Locally I don’t want to copy the secrets every time. I used python-dotenv: I create a .env file at the root and the script loads it on startup.
from dotenv import load_dotenv
load_dotenv() # On GitHub Actions there's no .env: it uses the secrets
The .env goes in .gitignore so credentials never reach the repo:
.env
.venv/
__pycache__/
And requirements.txt ended up like this:
requests
beautifulsoup4
google-auth
google-api-python-client
python-dotenv
The result
The full flow is: the cron starts the environment → main.py scrapes the El Mundo calendar → compares with Google Calendar → updates if there’s a change → alerts on Telegram when appropriate. All with no servers and no cost, from a repository called laliga-calendar-sync-bot.
Final lesson: parameterizing the team through environment variables turned a bot built for one specific club into a reusable project: anyone can fork it, set their team and division, and have the same system running.
End of the series!