-
Notifications
You must be signed in to change notification settings - Fork 178
feat(website): project website with nightly build tracking #651
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| name: Nightly Build & Log | ||
|
|
||
| # Builds the signed nightly APK on a schedule and records the result in the | ||
| # website build log (website/data/nightly_builds.json). Committing that file to | ||
| # main touches website/**, which triggers deploy-website.yml to refresh the | ||
| # published site. This complements the push-triggered F-Droid deploy | ||
| # (nightlydepolyci.yml); the two can be unified later. | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: '0 2 * * *' # 02:00 UTC daily | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: write # commit the updated build log back to main | ||
|
|
||
| jobs: | ||
| nightly: | ||
| name: Build nightly APK and record build log | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Free Disk Space (Ubuntu) | ||
| uses: jlumbroso/free-disk-space@main | ||
| with: | ||
| tool-cache: false | ||
| android: false # keep Android SDKs for Flutter | ||
| dotnet: true | ||
| haskell: true | ||
| large-packages: true | ||
| docker-images: true | ||
| swap-storage: true | ||
|
|
||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - uses: actions/setup-java@v4 | ||
| with: | ||
| distribution: temurin | ||
| java-version: '17.x' | ||
|
|
||
| - name: Setup Flutter | ||
| uses: subosito/flutter-action@v2 | ||
| with: | ||
| flutter-version: '3.29.2' | ||
|
|
||
| - name: Get dependencies | ||
| run: flutter pub get | ||
|
|
||
| - name: Decode signing secrets | ||
| env: | ||
| NIGHTLY_KEYSTORE_B64: ${{ secrets.NIGHTLY_KEYSTORE_B64 }} | ||
| NIGHTLY_PROPERTIES_B64: ${{ secrets.NIGHTLY_PROPERTIES_B64 }} | ||
| run: | | ||
| echo "$NIGHTLY_KEYSTORE_B64" | base64 --decode > android/nightly.jks | ||
| echo "$NIGHTLY_PROPERTIES_B64" | base64 --decode > android/key_nightly.properties | ||
|
|
||
| - name: Build nightly APK | ||
| id: build | ||
| run: flutter build apk --flavor nightly --build-number=${{ github.run_number }} --release | ||
|
|
||
| - name: Record successful build | ||
| if: success() | ||
| run: | | ||
| python3 scripts/update_build_log.py success \ | ||
| "https://github.com/${{ github.repository }}/raw/fdroid-repo/repo/nightly.${{ github.run_number }}.apk" \ | ||
| "${{ github.run_number }}" | ||
|
|
||
| - name: Record failed build | ||
| if: failure() | ||
| run: python3 scripts/update_build_log.py failure "" "${{ github.run_number }}" | ||
|
|
||
| - name: Commit build log | ||
| if: always() | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
| if git diff --quiet -- website/data/nightly_builds.json; then | ||
| echo "No build-log changes to commit." | ||
| else | ||
| git add website/data/nightly_builds.json | ||
| git commit -m "chore(nightly): record build ${{ github.run_number }}" | ||
| git push | ||
|
Comment on lines
+79
to
+84
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win The website deployment workflow will not be triggered by this push. The comment at the top of this file states: "Committing that file to main touches website/**, which triggers deploy-website.yml". To ensure the website is automatically rebuilt after the build log is updated, you can either:
🤖 Prompt for AI Agents |
||
| fi | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| name: Deploy Website | ||
|
|
||
| # Builds the Hugo site in website/ and publishes it to GitHub Pages. | ||
| # Triggered when the site content or this workflow changes (the nightly | ||
| # build-log commit made by build-nightly.yml touches website/data/, so a new | ||
| # nightly automatically refreshes the published site). | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - 'website/**' | ||
| - '.github/workflows/deploy-website.yml' | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pages: write | ||
| id-token: write | ||
|
|
||
| # Allow one concurrent deployment; don't cancel an in-progress production deploy. | ||
| concurrency: | ||
| group: pages | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| build: | ||
| runs-on: ubuntu-latest | ||
| env: | ||
| HUGO_VERSION: 0.164.0 | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 # hugo.toml enableGitInfo needs full history | ||
|
|
||
| - name: Setup Hugo | ||
| uses: peaceiris/actions-hugo@v3 | ||
| with: | ||
| hugo-version: ${{ env.HUGO_VERSION }} | ||
| extended: true | ||
|
|
||
| - name: Configure Pages | ||
| id: pages | ||
| uses: actions/configure-pages@v5 | ||
|
|
||
| - name: Build site | ||
| run: hugo --source website --minify --baseURL "${{ steps.pages.outputs.base_url }}/" | ||
|
|
||
| - name: Upload artifact | ||
| uses: actions/upload-pages-artifact@v3 | ||
| with: | ||
| path: website/public | ||
|
|
||
| deploy: | ||
| needs: build | ||
| runs-on: ubuntu-latest | ||
| environment: | ||
| name: github-pages | ||
| url: ${{ steps.deployment.outputs.page_url }} | ||
| steps: | ||
| - name: Deploy to GitHub Pages | ||
| id: deployment | ||
| uses: actions/deploy-pages@v4 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| #!/usr/bin/env python3 | ||
| """Record a nightly build in the website's build-log data file. | ||
|
|
||
| Prepends an entry describing the current commit to | ||
| ``website/data/nightly_builds.json`` (newest first) and keeps only the most | ||
| recent ``MAX_ENTRIES``. The Hugo ``nightly-builds`` shortcode renders this file. | ||
|
|
||
| Run from the repository root (as the CI workflow does): | ||
|
|
||
| python3 scripts/update_build_log.py <status> <artifact_url> <build_number> | ||
|
|
||
| All arguments are optional: | ||
| status "success" (default) or "failure" | ||
| artifact_url link to the built APK ("" if none) | ||
| build_number CI run number ("" if unknown) | ||
| """ | ||
|
|
||
| import json | ||
| import subprocess | ||
| import sys | ||
| from datetime import datetime, timezone | ||
| from pathlib import Path | ||
|
|
||
| LOG = Path("website/data/nightly_builds.json") | ||
| MAX_ENTRIES = 90 | ||
|
|
||
|
|
||
| def _git(*args: str) -> str: | ||
| return subprocess.check_output(["git", *args]).decode().strip() | ||
|
|
||
|
|
||
| def main(status: str = "success", artifact: str = "", build_number: str = "") -> None: | ||
| entry = { | ||
| "ts": datetime.now(timezone.utc).isoformat(timespec="seconds"), | ||
| "sha": _git("rev-parse", "HEAD")[:8], | ||
| "msg": _git("log", "-1", "--pretty=%s"), | ||
| "status": status, | ||
| "artifact": artifact, | ||
| "build": build_number, | ||
| } | ||
|
|
||
| entries = [] | ||
| if LOG.exists(): | ||
| try: | ||
| entries = json.loads(LOG.read_text() or "[]") | ||
| except json.JSONDecodeError: | ||
| entries = [] | ||
|
|
||
| entries.insert(0, entry) | ||
| entries = entries[:MAX_ENTRIES] | ||
|
|
||
| LOG.parent.mkdir(parents=True, exist_ok=True) | ||
| LOG.write_text(json.dumps(entries, indent=2) + "\n") | ||
| print( | ||
| f"Recorded nightly build {entry['sha']} ({status}); " | ||
| f"{len(entries)} entr{'y' if len(entries) == 1 else 'ies'} in {LOG}." | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main(*sys.argv[1:]) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # Hugo build output and caches | ||
| /public/ | ||
| /resources/ | ||
| .hugo_build.lock |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # TaskWarrior Mobile — Project Website | ||
|
|
||
| A self-contained [Hugo](https://gohugo.io) static site for the TaskWarrior Mobile | ||
| app: landing page, downloads (with an auto-updated **nightly build log**), and a | ||
| documentation section. It has **no external theme** — all layouts live in | ||
| `layouts/`, so it builds from the single Hugo binary with no submodules or network | ||
| fetches. | ||
|
|
||
| ## Local development | ||
|
|
||
| Requires Hugo **extended** (pinned to the version in | ||
| [`.github/workflows/deploy-website.yml`](../.github/workflows/deploy-website.yml)): | ||
|
|
||
| ```bash | ||
| # from this directory | ||
| hugo server # live-reload dev server at http://localhost:1313 | ||
| hugo --minify # production build into ./public | ||
| ``` | ||
|
|
||
| ## Layout | ||
|
|
||
| ``` | ||
| website/ | ||
| ├── hugo.toml # site config (baseURL, menus, params) | ||
| ├── content/ # Markdown pages (_index, downloads, docs) | ||
| ├── layouts/ # self-contained templates (no theme) | ||
| │ ├── _default/ # baseof / single / list | ||
| │ ├── index.html # homepage | ||
| │ └── shortcodes/ | ||
| │ └── nightly-builds.html # renders data/nightly_builds.json | ||
| ├── data/ | ||
| │ └── nightly_builds.json # build log (populated by CI; ships empty) | ||
| └── static/ | ||
| ├── css/style.css | ||
| └── CNAME # custom domain | ||
| ``` | ||
|
|
||
| ## Nightly build log | ||
|
|
||
| [`scripts/update_build_log.py`](../scripts/update_build_log.py) prepends an entry | ||
| `{ts, sha, msg, status, artifact, build}` to `data/nightly_builds.json` (keeping the | ||
| 90 most recent). The `{{</* nightly-builds */>}}` shortcode on the downloads page | ||
| renders it. The file ships as `[]`; CI fills it in. | ||
|
|
||
| ## CI | ||
|
|
||
| - **`build-nightly.yml`** — daily at 02:00 UTC (and on demand): builds the signed | ||
| nightly APK, records the result via `update_build_log.py`, and commits the updated | ||
| log to `main`. | ||
| - **`deploy-website.yml`** — on any push touching `website/**` (including the nightly | ||
| log commit): builds the site with Hugo and deploys it to GitHub Pages. | ||
| - `nightlydepolyci.yml` now ignores `website/**` and `scripts/**` so a build-log | ||
| commit does not trigger a redundant APK rebuild. | ||
|
|
||
| ## One-time infrastructure setup (needs repo-admin / DNS access) | ||
|
|
||
| These cannot be done from code and must be configured by a maintainer: | ||
|
|
||
| 1. **Enable GitHub Pages** → repo *Settings → Pages → Build and deployment → | ||
| Source: **GitHub Actions***. | ||
| 2. **Custom domain / DNS** — point `taskwarrior.ccextractor.org` at GitHub Pages | ||
| with a `CNAME` DNS record to `<org-or-user>.github.io`. The site already ships a | ||
| `static/CNAME`, so Pages will pick up the domain on first deploy. | ||
| 3. **Branch protection** — if `main` is protected, allow `github-actions[bot]` to | ||
| push the nightly build-log commit (or point that commit at an unprotected branch). | ||
| 4. The nightly signing secrets (`NIGHTLY_KEYSTORE_B64`, `NIGHTLY_PROPERTIES_B64`) are | ||
| the same ones the existing F-Droid workflow already uses. | ||
|
|
||
| > **Note on the deploy target.** The proposal described renaming the `fdroid-repo` | ||
| > branch to `public-site` and serving from a branch. This implementation instead uses | ||
| > the modern **GitHub Actions Pages deployment** (`actions/deploy-pages`), which keeps | ||
| > the site source in `website/` on `main` and needs no dedicated deploy branch — fewer | ||
| > moving parts and no history juggling. The F-Droid APK repo on `fdroid-repo` is | ||
| > untouched. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| --- | ||
| title: "TaskWarrior Mobile" | ||
| --- | ||
|
|
||
| **TaskWarrior Mobile** is the cross-platform [Flutter](https://flutter.dev) client for | ||
| [TaskWarrior](https://taskwarrior.org), maintained by [CCExtractor](https://ccextractor.org). | ||
| It brings TaskWarrior's fast, unobtrusive task management to Android, iOS, Windows, | ||
| macOS, and Linux, backed by [TaskChampion](https://github.com/GothenburgBitFactory/taskchampion) | ||
| for native, offline-first sync. | ||
|
|
||
| New here? Head to the [downloads page](downloads/) to install the latest build, or browse | ||
| the [nightly build history](downloads/#nightly-builds) to see what has changed recently. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| --- | ||
| title: "Documentation" | ||
| description: "Developer and user documentation for TaskWarrior Mobile." | ||
| --- | ||
|
|
||
| Documentation for TaskWarrior Mobile — a living set of guides that grows alongside the | ||
| app. New here? Start with [Getting started](getting-started/), then | ||
| [set up sync](sync-setup/) when you are ready to connect a server. Curious how it all | ||
| fits together? See the [architecture overview](architecture/). | ||
|
|
||
| ## Contributing | ||
|
|
||
| Contributions are welcome. Start with the | ||
| [CONTRIBUTING guide](https://github.com/CCExtractor/taskwarrior-flutter/blob/main/CONTRIBUTING.md) | ||
| in the repository, and join the [CCExtractor community](https://ccextractor.org/) on Slack | ||
| or Zulip. | ||
|
|
||
| ## Guides |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| --- | ||
| title: "Architecture" | ||
| description: "How TaskWarrior Mobile is built — Flutter UI over a native Rust core." | ||
| weight: 3 | ||
| --- | ||
|
|
||
| TaskWarrior Mobile is a Flutter app sitting on top of a native Rust core, so the same | ||
| task engine that powers TaskWarrior on the desktop runs unchanged on your phone. | ||
|
|
||
| ## Layers | ||
|
|
||
| - **UI — Flutter (Dart).** Screens, navigation, and state management use | ||
| [GetX](https://pub.dev/packages/get). This layer never talks to the network for task | ||
| data; it reads and writes through the core. | ||
| - **Core — Rust (`tc_helper`).** A thin wrapper around | ||
| [TaskChampion](https://github.com/GothenburgBitFactory/taskchampion) exposed to Dart | ||
| through [`flutter_rust_bridge`](https://github.com/fzyzcjy/flutter_rust_bridge). It | ||
| owns task storage, mutations, and sync. | ||
| - **Storage & sync — TaskChampion.** Tasks live in a local replica on the device. | ||
| Syncing reconciles that replica with a TaskChampion sync server; there is no | ||
| bespoke HTTP API in between. | ||
|
|
||
| ## Offline-first data flow | ||
|
|
||
| ``` | ||
| tap "complete" ─▶ Flutter ─▶ Rust FFI ─▶ local replica (instant, no network) | ||
| │ | ||
| (later, when online) | ||
| ▼ | ||
| TaskChampion sync server | ||
| ``` | ||
|
|
||
| Every create, edit, and complete is committed to the local replica synchronously, so the | ||
| UI never waits on the network. Synchronisation is a separate, best-effort step that runs | ||
| when connectivity is available. | ||
|
|
||
| ## Why a Rust core? | ||
|
|
||
| Reusing TaskChampion means the mobile app shares TaskWarrior's battle-tested task model — | ||
| recurrence, dependencies, annotations, tags, and the sync protocol — instead of | ||
| reimplementing them. The Rust bridge surfaces those capabilities to the Flutter UI with a | ||
| small, typed FFI surface. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift
Scheduled nightly APKs are never pushed to
fdroid-repo.This workflow builds the APK and records its URL as being located in the
fdroid-repobranch, but it never actually pushes the APK to that branch (unlikenightlydepolyci.yml). As a result, the APK is discarded when the runner terminates, and the recorded download link will return a 404.To fix this, you must either replicate the
fdroid-repodeployment steps fromnightlydepolyci.ymlhere before recording the success, or unify the two workflows immediately so that building, F-Droid deployment, and website logging always occur together.🤖 Prompt for AI Agents