Automatically sync your recent Strava activities to a calendar (.ics) file using GitHub Actions.
Because modern calendar apps (Google Calendar, Apple Calendar) require a stable URL to subscribe to, this tool pushes your calendar data to a pseudo-private GitHub Secret Gist. This keeps your main repository private while allowing your calendar app to fetch the data.
- A free Strava account.
- A GitHub account.
- Basic familiarity with the terminal (for one-time setup).
- Log in to Strava API Settings.
- Create an Application:
- Name:
Strava2Cal(or anything you like). - Website:
http://localhost. - Authorization Callback Domain:
localhost. - Icon: You MUST upload an image/icon, or Strava won't show your keys.
- Name:
- Copy your Client ID and Client Secret.
This one-time step authorizes the script to access your data forever.
Important
If you plan to use the BasicFit → Strava import, you need the scope activity:write in addition to activity:read_all. Use the URL below which includes both.
- Generate the Authorization URL:
Replace
[YOUR_CLIENT_ID]in the URL below with your actual ID, then paste it into your browser:https://www.strava.com/oauth/authorize?client_id=[YOUR_CLIENT_ID]&response_type=code&redirect_uri=http://localhost/exchange_token&approval_prompt=force&scope=activity:read_all,activity:write - Authorize: Click "Authorize" on the Strava page. You will be redirected to a broken page (
localhost). - Get the Code: Look at the URL bar of the broken page. Copy the code after
&code=.- Example:
...&code=a1b2c3d4e5f6...-> Copya1b2c3d4e5f6.
- Example:
- Exchange for Refresh Token:
Run this command in your terminal (replacing values):
curl -X POST https://www.strava.com/oauth/token \ -F client_id=[YOUR_CLIENT_ID] \ -F client_secret=[YOUR_CLIENT_SECRET] \ -F code=[CODE_FROM_STEP_3] \ -F grant_type=authorization_code
- Copy the
"refresh_token"from the JSON response.
- Go to Settings > Developer settings > Personal access tokens > Tokens (classic).
- Generate new token (classic):
- Note:
Strava Gist Sync - Expiration:
No expiration - Scopes: Check
gist. - Copy the token (starts with
ghp_).
- Note:
- Go to gist.github.com.
- Description:
My Strava Calendar - Filename:
strava.ics - Content:
init - Click: "Create secret gist".
- Description:
- Copy the Gist ID from the URL (the long string at the end of the URL).
- Fork this repository to your account.
- Go to Settings > Secrets and variables > Actions.
- Add these 5 Repository Secrets:
STRAVA_CLIENT_IDSTRAVA_CLIENT_SECRETSTRAVA_REFRESH_TOKENGIST_TOKEN(Yourghp_token)GIST_ID
- Go to the Actions tab and enable workflows if prompted.
- Select the Update Strava Calendar workflow and click Run workflow.
Since you have forked the project, you have full control over the code! You can modify sync_strava.py to customize your calendar events.
- Change Emojis: Edit the
create_ics_contentfunction (around line 53) to swap 🏃/🚴 for other symbols. - Filter Activities: Want to ignore commutes or only sync runs? Add a simple
ifcondition in the loop. - Change Descriptions: Modify what info appears in the calendar event (add heart rate, calories, etc.) by editing the
e.descriptionfield. - Sync ALL History: By default, only the last 50 activities are synced. To sync EVERYTHING, add a repository secret (or variable) named
SYNC_FULL_HISTORYwith the valuetrue.
By default, the calendar updates every hour. GitHub Actions requires the schedule to be defined in the workflow file itself (it cannot be a variable or secret).
To change the frequency:
- Open the file
.github/workflows/update_calendar.yml. - Look for line 5:
- cron: "0 * * * *". - Change the value inside the quotes.
| Frequency | Cron Value | Description |
|---|---|---|
| Every Hour (Default) | "0 * * * *" |
Runs at minute 0 of every hour. |
| Every 30 Minutes | "*/30 * * * *" |
Runs at minute 0 and 30. |
| Every 15 Minutes | "*/15 * * * *" |
Runs at 0, 15, 30, 45. |
| Once a Day (8 AM) | "0 8 * * *" |
Runs daily at 08:00 UTC. |
| Every 6 Hours | "0 */6 * * *" |
Runs at 00:00, 06:00, 12:00, 18:00. |
Note: GitHub Actions applies a random delay (usually 5-10 mins) during high load.
Once the workflow runs successfully (green checkmark), your Gist will be updated.
- Go to your Gist and open the
strava.icsfile. - Click the Raw button.
- Copy the URL.
- Note: To make the link permanent even if you update the Gist, remove the commit hash from the URL.
- Global URL:
https://gist.githubusercontent.com/[USER]/[GIST_ID]/raw/strava.ics
- Paste this URL into your calendar app (Google Calendar: Add from URL, Apple Calendar: New Subscription).
sync_strava.py: Fetches Strava data and formats the ICS calendar.compare_basicfit_strava.py: Compare BasicFit sessions with Strava and import missing ones..github/workflows/update_calendar.yml: GitHub Actions scheduler (Cron).pyproject.toml/uv.lock: Python dependencies managed withuv.README.md: This file.
Le script compare_basicfit_strava.py compare tes séances BasicFit (ICS) avec tes
activités Strava (ICS), détecte les séances non trackées, et peut les uploader sur Strava.
uv sync # installe les dépendancesLes variables d'environnement Strava sont nécessaires uniquement pour l'upload :
| Variable | Description |
|---|---|
STRAVA_CLIENT_ID |
ID de ton application Strava |
STRAVA_CLIENT_SECRET |
Secret de ton application Strava |
STRAVA_REFRESH_TOKEN |
Refresh token (voir Phase 2 du Setup) |
Les deux arguments --basicfit et --strava acceptent soit un chemin de fichier local,
soit une URL (ex. lien Raw d'un Gist GitHub).
| Source | Contenu |
|---|---|
| BasicFit ICS | DTSTART = heure d'arrivée, DTEND = heure de départ réel |
| Strava ICS | Filtre automatiquement les events 💪 * Weight Training |
uv run python compare_basicfit_strava.py \
--basicfit "https://gist.githubusercontent.com/.../basicfit.ics" \
--strava "https://gist.githubusercontent.com/.../strava.ics"Affiche un tableau des séances matchées ✅ et des séances manquantes ❌.
Génère aussi un fichier preview_upload.json avec ce qui serait uploadé.
uv run python compare_basicfit_strava.py ... --from 2025-10-01Par défaut ±30 minutes entre l'arrivée BasicFit et le début du timer Strava.
uv run python compare_basicfit_strava.py ... --margin 45export STRAVA_CLIENT_ID=...
export STRAVA_CLIENT_SECRET=...
export STRAVA_REFRESH_TOKEN=...
uv run python compare_basicfit_strava.py \
--basicfit basicfit.ics \
--strava strava.ics \
--uploadWarning
Erreur 401 Unauthorized lors de l'upload ? Ton refresh token n'a pas le scope activity:write.
Regénère-le avec l'URL de la Phase 2 ci-dessus (qui inclut activity:write), puis mets à jour
le secret STRAVA_REFRESH_TOKEN dans GitHub Actions.
Une confirmation est demandée avant tout envoi. Chaque séance importée crée une activité
WeightTraining nommée 💪 Muscu BasicFit avec la durée réelle du ICS BasicFit.
Le workflow "Import BasicFit → Strava" se déclenche manuellement uniquement depuis l'onglet Actions.
En plus des secrets Strava existants, ajoute ces 2 secrets dans Settings → Secrets → Actions :
| Secret | Valeur |
|---|---|
BASICFIT_ICS_URL |
URL Raw du Gist BasicFit |
STRAVA_ICS_URL |
URL Raw du Gist Strava |
- Aller dans l'onglet Actions → Import BasicFit → Strava
- Cliquer Run workflow
- Remplir les options :
| Option | Défaut | Description |
|---|---|---|
dry_run |
✅ coché | Affiche sans uploader, génère un artifact preview_upload.json |
margin |
30 |
Tolérance en minutes entre arrivée BF et début Strava |
since |
(vide) | N'importer que depuis cette date (YYYY-MM-DD) |
Décocher
dry_runpour uploader réellement les séances manquantes sur Strava.
| Option | Défaut | Description |
|---|---|---|
--basicfit |
(requis) | Fichier/URL ICS BasicFit |
--strava |
(requis) | Fichier/URL ICS Strava |
--margin |
30 |
Tolérance en minutes |
--duration |
90 |
Durée fallback (min) si non dispo dans le ICS |
--from |
(tout) | Ne comparer qu'à partir de cette date (YYYY-MM-DD) |
--upload |
off | Uploader sur Strava |
--yes / -y |
off | Bypass la confirmation interactive (pour CI) |
--preview |
preview_upload.json |
Fichier JSON de prévisualisation |
| Fichier | Contenu |
|---|---|
preview_upload.json |
Liste des activités qui seraient uploadées (toujours généré) |
uploaded_activities.json |
Réponses de l'API Strava après upload réussi |