From ff70dcd05b2c3ebd250a3b556bfa127a5db7befc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Sun, 12 Jul 2026 01:40:50 +0200 Subject: [PATCH 1/3] chore: bump checkout->v5 and setup-python->v6 (Node 24 native) v4/v5 still target Node 20, which the runners now force onto Node 24 with a deprecation warning. v5/v6 are the Node-24-native majors and clear it. --- .github/workflows/pages.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index a1991aed..e68e31c4 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -19,13 +19,13 @@ jobs: python_version: ['3.12'] steps: - name: Checkout 🛎️ - uses: actions/checkout@v4 # persist-credentials must be false so the deploy step below uses its own token, not the checkout credentials. + uses: actions/checkout@v5 # persist-credentials must be false so the deploy step below uses its own token, not the checkout credentials. with: submodules: 'recursive' persist-credentials: false - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python_version }} From e1db6dced841a606340370678a28e04d41d5a804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Sun, 12 Jul 2026 01:43:52 +0200 Subject: [PATCH 2/3] feat: show newer pre-release on downloads page, build links from real assets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The downloads page only showed the latest stable (prereleases were filtered out), and update-downloads.py generated links by substituting the version number into a fixed template — which breaks when asset naming changes (v0.14 ships 'tauri' builds and moved macOS to arm64, so substitution would produce dead -macos-x86_64.dmg links). Rewrite update-downloads.py to build each platform's links from the release's actual assets, and emit a 'prerelease' block whenever the latest pre-release is newer than the latest stable. downloads.yml is now {stable, prerelease?}; downloads.pug renders a 'Pre-release: vX' section when present. Package-manager and Play Store links stay on the stable block only. Verified: all 14 generated GitHub asset URLs return 200, including the v0.14.0b1 arm64 macOS builds. --- _data/downloads.yml | 94 ++++++++++++++-------- downloads.pug | 21 ++++- scripts/update-downloads.py | 156 ++++++++++++++++++++++++++++++------ 3 files changed, 210 insertions(+), 61 deletions(-) diff --git a/_data/downloads.yml b/_data/downloads.yml index a0b77142..f2c0c393 100644 --- a/_data/downloads.yml +++ b/_data/downloads.yml @@ -1,42 +1,68 @@ -version: v0.12.2 - -platforms: +# Generated by scripts/update-downloads.py — do not edit by hand. +stable: + version: v0.13.2 + platforms: - name: Windows assets: - - title: Installer - description: " (recommended)" - url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.12.2/activitywatch-v0.12.2-windows-x86_64-setup.exe - - title: .zip - url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.12.2/activitywatch-v0.12.2-windows-x86_64.zip - - title: Chocolatey - url: https://chocolatey.org/packages/activitywatch - + - title: Installer + url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64-setup.exe + description: ' (recommended)' + - title: .zip + url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip + - title: Chocolatey + url: https://chocolatey.org/packages/activitywatch - name: Linux assets: - - title: .zip - url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.12.2/activitywatch-v0.12.2-linux-x86_64.zip - - title: .AppImage - url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.12.2/activitywatch-linux-x86_64.AppImage - - title: AUR - description: ", for Arch Linux and Manjaro" - url: https://aur.archlinux.org/packages/activitywatch-bin/ - - title: nixpkgs - description: ", for NixOS" - url: https://search.nixos.org/packages?query=activitywatch&show=activitywatch - + - title: .zip + url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-linux-x86_64.zip + - title: .AppImage + url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-linux-x86_64.AppImage + - title: .deb + url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-linux-x86_64.deb + - title: AUR + description: ', for Arch Linux and Manjaro' + url: https://aur.archlinux.org/packages/activitywatch-bin/ + - title: nixpkgs + description: ', for NixOS' + url: https://search.nixos.org/packages?query=activitywatch&show=activitywatch - name: macOS assets: - - title: .dmg - description: " (recommended)" - url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.12.2/activitywatch-v0.12.2-macos-x86_64.dmg - - type: zip - title: .zip - url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.12.2/activitywatch-v0.12.2-macos-x86_64.zip - - title: Homebrew - url: https://formulae.brew.sh/cask/activitywatch - + - title: .dmg + url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-macos-x86_64.dmg + description: ' (recommended) (Intel)' + - title: .zip + url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-macos-x86_64.zip + description: ' (Intel)' + - title: Homebrew + url: https://formulae.brew.sh/cask/activitywatch - name: Android assets: - - title: Play Store - description: " (beta)" - url: https://play.google.com/store/apps/details?id=net.activitywatch.android + - title: Play Store + description: ' (beta)' + url: https://play.google.com/store/apps/details?id=net.activitywatch.android +prerelease: + version: v0.14.0b1 + platforms: + - name: Windows + assets: + - title: Installer + url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.14.0b1/activitywatch-v0.14.0b1-windows-x86_64-setup.exe + description: ' (recommended)' + - title: .zip + url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.14.0b1/activitywatch-v0.14.0b1-windows-x86_64.zip + - name: Linux + assets: + - title: .zip + url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.14.0b1/activitywatch-v0.14.0b1-linux-x86_64.zip + - title: .AppImage + url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.14.0b1/activitywatch-linux-x86_64.AppImage + - title: .deb + url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.14.0b1/activitywatch-v0.14.0b1-linux-x86_64.deb + - name: macOS + assets: + - title: .dmg + url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.14.0b1/activitywatch-v0.14.0b1-macos-arm64.dmg + description: ' (recommended) (Apple Silicon)' + - title: .zip + url: https://github.com/ActivityWatch/activitywatch/releases/download/v0.14.0b1/activitywatch-v0.14.0b1-macos-arm64.zip + description: ' (Apple Silicon)' diff --git a/downloads.pug b/downloads.pug index 85fe5d0e..6b9001db 100644 --- a/downloads.pug +++ b/downloads.pug @@ -9,10 +9,10 @@ article.container header h1 Downloads - h3.mt-4.mb-2 ActivityWatch {{ site.data.downloads.version }} + h3.mt-4.mb-2 ActivityWatch {{ site.data.downloads.stable.version }} div.row.mt-4 - | {% for platform in site.data.downloads.platforms %} + | {% for platform in site.data.downloads.stable.platforms %} div.col-md-6(id="{{ platform.name }}") b {{ platform.name }} ul @@ -23,6 +23,23 @@ article.container | {% endfor %} | {% endfor %} + | {% if site.data.downloads.prerelease %} + h3.mt-5.mb-2 Pre-release: ActivityWatch {{ site.data.downloads.prerelease.version }} + p.small.dim + | A newer pre-release is available for testing new features and fixes. It may be unstable — for everyday use, prefer the stable release above. + div.row.mt-2 + | {% for platform in site.data.downloads.prerelease.platforms %} + div.col-md-6(id="prerelease-{{ platform.name }}") + b {{ platform.name }} + ul + | {% for asset in platform.assets %} + li + a(href="{{asset.url}}" style="{% if forloop.first %} font-weight: bold {% endif %}") {{ asset.title }} + | {{ asset.description }} + | {% endfor %} + | {% endfor %} + | {% endif %} + div For help on how to get started, see the #[a(href="https://docs.activitywatch.net/en/latest/getting-started.html") Getting Started guide] in the documentation. div Looking for an older or pre-release version? You can find all #[a(href="https://github.com/ActivityWatch/activitywatch/releases") releases on GitHub]. diff --git a/scripts/update-downloads.py b/scripts/update-downloads.py index 80c222e1..ef667e43 100755 --- a/scripts/update-downloads.py +++ b/scripts/update-downloads.py @@ -1,43 +1,149 @@ #!/usr/bin/env python3 +"""Regenerate _data/downloads.yml from the ActivityWatch GitHub releases. +Writes the latest stable release, plus — when it is newer than that stable — +the latest pre-release, so betas/nightlies newer than stable are surfaced too. + +Download links are built from each release's *actual* assets rather than by +substituting a version number into a fixed template, so it survives asset +naming changes between versions (e.g. the v0.14 `tauri` builds and macOS +moving from x86_64 to arm64). +""" + +import os import re from datetime import datetime, timedelta, timezone from pathlib import Path import requests +import yaml +API = "https://api.github.com/repos/ActivityWatch/activitywatch/releases" -def is_older(dt: str, td: timedelta): - now = datetime.now(tz=timezone.utc) - created_at = datetime.fromisoformat(dt.strip("Z")).replace(tzinfo=timezone.utc) - return created_at < now - td +# Static (non-GitHub) distribution links, appended per platform for the stable +# release only — package managers and the Play Store track stable, not betas. +PACKAGE_LINKS = { + "Windows": [ + {"title": "Chocolatey", "url": "https://chocolatey.org/packages/activitywatch"}, + ], + "Linux": [ + {"title": "AUR", "description": ", for Arch Linux and Manjaro", + "url": "https://aur.archlinux.org/packages/activitywatch-bin/"}, + {"title": "nixpkgs", "description": ", for NixOS", + "url": "https://search.nixos.org/packages?query=activitywatch&show=activitywatch"}, + ], + "macOS": [ + {"title": "Homebrew", "url": "https://formulae.brew.sh/cask/activitywatch"}, + ], +} +# Android has no GitHub release assets; it's always just the Play Store link. +ANDROID = { + "name": "Android", + "assets": [ + {"title": "Play Store", "description": " (beta)", + "url": "https://play.google.com/store/apps/details?id=net.activitywatch.android"}, + ], +} -if __name__ == "__main__": - r = requests.get( - "https://api.github.com/repos/ActivityWatch/activitywatch/releases" - ) - data = r.json() +# Per platform: (asset-name regex, button title, extra description). +# description may be a callable taking the matched asset. +PLATFORM_SPECS = { + "Windows": [ + (r"windows.*setup\.exe$", "Installer", " (recommended)"), + (r"windows.*\.zip$", ".zip", ""), + ], + "Linux": [ + (r"linux.*x86_64\.zip$", ".zip", ""), + (r"\.AppImage$", ".AppImage", ""), + (r"\.deb$", ".deb", ""), + ], + "macOS": [ + (r"macos.*\.dmg$", ".dmg", lambda a: " (recommended)" + _arch(a)), + (r"macos.*\.zip$", ".zip", lambda a: _arch(a)), + ], +} - # Filter away prereleases - data = filter(lambda r: not r["prerelease"] and not r["draft"], data) - # Filter away very recent releases (<1h old) - data = filter(lambda r: is_older(r["created_at"], timedelta(hours=1)), data) +def _arch(asset: dict) -> str: + name = asset["name"].lower() + if "arm64" in name or "aarch64" in name: + return " (Apple Silicon)" + if "x86_64" in name or "amd64" in name: + return " (Intel)" + return "" - data = sorted(data, key=lambda r: r["created_at"]) - latest_release = data[-1]["tag_name"] - print(f"Latest release: {latest_release}") - p = Path("_data/downloads.yml") +def _pick(assets: list, pattern: str): + """First asset matching pattern, preferring the plain (non-tauri) build.""" + cands = [a for a in assets if re.search(pattern, a["name"], re.I)] + cands.sort(key=lambda a: ("tauri" in a["name"].lower(), len(a["name"]))) + return cands[0] if cands else None + + +def platforms_for(release: dict, include_packages: bool) -> list: + assets = release["assets"] + platforms = [] + for name, specs in PLATFORM_SPECS.items(): + entries = [] + for pattern, title, desc in specs: + a = _pick(assets, pattern) + if not a: + continue + entry = {"title": title, "url": a["browser_download_url"]} + d = desc(a) if callable(desc) else desc + if d: + entry["description"] = d + entries.append(entry) + if include_packages: + entries += PACKAGE_LINKS.get(name, []) + if entries: + platforms.append({"name": name, "assets": entries}) + return platforms + + +def build_block(release: dict, include_packages: bool, with_android: bool) -> dict: + platforms = platforms_for(release, include_packages) + if with_android: + platforms.append(ANDROID) + return {"version": release["tag_name"], "platforms": platforms} + - with open(p, "r") as f: - orig = f.read() - new = re.sub(r"v[0-9.]+[abrc]*[0-9]*", latest_release, orig) +def is_older(dt: str, td: timedelta) -> bool: + created = datetime.fromisoformat(dt.replace("Z", "+00:00")) + return created < datetime.now(tz=timezone.utc) - td - if new == orig: - print("No change") + +if __name__ == "__main__": + headers = {"Accept": "application/vnd.github+json"} + token = os.getenv("GITHUB_TOKEN") + if token: + headers["Authorization"] = f"Bearer {token}" + releases = requests.get(API, headers=headers, timeout=30).json() + + # Ignore drafts and anything <1h old (assets may still be uploading). + releases = [r for r in releases + if not r["draft"] and is_older(r["created_at"], timedelta(hours=1))] + releases.sort(key=lambda r: r["created_at"]) + + stable = [r for r in releases if not r["prerelease"]] + if not stable: + raise SystemExit("No stable release found") + latest_stable = stable[-1] + + data = {"stable": build_block(latest_stable, include_packages=True, with_android=True)} + + # Surface the latest pre-release only if it's newer than the latest stable. + newer_pre = [r for r in releases + if r["prerelease"] and r["created_at"] > latest_stable["created_at"]] + if newer_pre: + latest_pre = newer_pre[-1] + data["prerelease"] = build_block(latest_pre, include_packages=False, with_android=False) + print(f"Stable: {latest_stable['tag_name']}; newer pre-release: {latest_pre['tag_name']}") else: - with open(p, "w") as f: - f.write(new) - print(f"Wrote to {p}") + print(f"Stable: {latest_stable['tag_name']} (no newer pre-release)") + + p = Path("_data/downloads.yml") + header = "# Generated by scripts/update-downloads.py — do not edit by hand.\n" + p.write_text(header + yaml.safe_dump(data, sort_keys=False, allow_unicode=True)) + print(f"Wrote {p}") From e4749b4025ab25519ba06d09c0b49408ebc4df48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Sun, 12 Jul 2026 03:32:03 +0200 Subject: [PATCH 3/3] fix(downloads): migrate remaining consumers to stable.*, harden API errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review on #62: - download-url.js (homepage download button) and schema-index.html (JSON-LD) still read the old flat site.data.downloads.version/.platforms, which the stable/prerelease split removed — point them at .stable.* so the CTA and schema keep working. - update-downloads.py: raise_for_status() + validate the response is a list, so a GitHub API error body (rate-limit/auth/5xx) fails loudly instead of being parsed as releases and crashing at r['draft']. --- _includes/download-url.js | 4 ++-- _includes/schema-index.html | 8 ++++---- scripts/update-downloads.py | 6 +++++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/_includes/download-url.js b/_includes/download-url.js index 016571ff..1835de20 100644 --- a/_includes/download-url.js +++ b/_includes/download-url.js @@ -26,8 +26,8 @@ function getOS() { function getDownloadInfo() { let os = getOS(); if (os !== null) { - let version = "{{ site.data.downloads.version }}"; - let platforms = JSON.parse(`{{ site.data.downloads.platforms | jsonify }}`); + let version = "{{ site.data.downloads.stable.version }}"; + let platforms = JSON.parse(`{{ site.data.downloads.stable.platforms | jsonify }}`); let p = platforms.filter((p) => p.name === os)[0] || null; if (p !== null) { return { diff --git a/_includes/schema-index.html b/_includes/schema-index.html index 2e11f89e..6f50f521 100644 --- a/_includes/schema-index.html +++ b/_includes/schema-index.html @@ -17,7 +17,7 @@ "ratingValue": "{{ site.data.ratings.alternativeto.average }}", "reviewCount": "{{ site.data.ratings.alternativeto.count }}" }, - "softwareVersion": "{{ site.data.downloads.version }}", + "softwareVersion": "{{ site.data.downloads.stable.version }}", "datePublished": "2024-06-12", "dateCreated": "2016-06-10", "downloadUrl": "https://activitywatch.net/downloads/", @@ -53,15 +53,15 @@ "target": [ "https://activitywatch.net/downloads/", "android-app:https://play.google.com/store/apps/details?id=net.activitywatch.android", - {% for platform in site.data.downloads.platforms %} + {% for platform in site.data.downloads.stable.platforms %} {% for asset in platform.assets %} { "@type": "EntryPoint", "urlTemplate": "{{ asset.url }}", "actionApplication": { "@type": "SoftwareApplication", - "@id": "activitywatch-{{ site.data.downloads.version }}-{{ platform.name | downcase }}-{{ asset.title | replace: ".", "" | downcase }}", - "name": "ActivityWatch {{ site.data.downloads.version }} {{ platform.name }} {{ asset.title }}", + "@id": "activitywatch-{{ site.data.downloads.stable.version }}-{{ platform.name | downcase }}-{{ asset.title | replace: ".", "" | downcase }}", + "name": "ActivityWatch {{ site.data.downloads.stable.version }} {{ platform.name }} {{ asset.title }}", "operatingSystem": "{{ platform.name }}" } }, diff --git a/scripts/update-downloads.py b/scripts/update-downloads.py index ef667e43..88ab5644 100755 --- a/scripts/update-downloads.py +++ b/scripts/update-downloads.py @@ -119,7 +119,11 @@ def is_older(dt: str, td: timedelta) -> bool: token = os.getenv("GITHUB_TOKEN") if token: headers["Authorization"] = f"Bearer {token}" - releases = requests.get(API, headers=headers, timeout=30).json() + resp = requests.get(API, headers=headers, timeout=30) + resp.raise_for_status() # fail loudly on rate-limit/auth/5xx, don't parse an error body + releases = resp.json() + if not isinstance(releases, list): + raise SystemExit(f"Unexpected GitHub API response (not a release list): {releases}") # Ignore drafts and anything <1h old (assets may still be uploading). releases = [r for r in releases