Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .github/workflows/build-nightly.yml
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 }}"
Comment on lines +66 to +68

Copy link
Copy Markdown

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-repo branch, but it never actually pushes the APK to that branch (unlike nightlydepolyci.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-repo deployment steps from nightlydepolyci.yml here 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-nightly.yml around lines 66 - 68, Update the
scheduled nightly workflow so the built APK is deployed to the fdroid-repo
branch before the success call to scripts/update_build_log.py. Reuse the
existing deployment steps and configuration from nightlydepolyci.yml, or unify
the workflows, ensuring the recorded nightly APK URL points to an actually
pushed artifact.


- 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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".
However, GitHub Actions intentionally prevents commits made with the default GITHUB_TOKEN from spawning new workflow runs. Because actions/checkout defaults to using this token, the git push on line 84 will not trigger deploy-website.yml.

To ensure the website is automatically rebuilt after the build log is updated, you can either:

  1. Provide a Personal Access Token (PAT) to the actions/checkout step (token: ${{ secrets.PAT }}) so the push is authenticated as a user.
  2. Trigger the deployment explicitly at the end of this job using the GitHub CLI (gh workflow run deploy-website.yml), assuming the default token is given actions: write scope.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-nightly.yml around lines 79 - 84, Update the nightly
workflow’s repository authentication so the commit and push of
website/data/nightly_builds.json can trigger deploy-website.yml. Configure the
existing actions/checkout step to use the repository’s PAT secret instead of the
default GITHUB_TOKEN, preserving the current commit and push flow.

fi
64 changes: 64 additions & 0 deletions .github/workflows/deploy-website.yml
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
7 changes: 7 additions & 0 deletions .github/workflows/nightlydepolyci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ on:
push:
branches:
- main
# Website content and the nightly build-log commit must not trigger a full
# APK rebuild + F-Droid redeploy (see build-nightly.yml / deploy-website.yml).
paths-ignore:
- 'website/**'
- 'scripts/**'
- '.github/workflows/deploy-website.yml'
- '.github/workflows/build-nightly.yml'

jobs:
build-and-deploy:
Expand Down
61 changes: 61 additions & 0 deletions scripts/update_build_log.py
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:])
4 changes: 4 additions & 0 deletions website/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Hugo build output and caches
/public/
/resources/
.hugo_build.lock
74 changes: 74 additions & 0 deletions website/README.md
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.
12 changes: 12 additions & 0 deletions website/content/_index.md
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.
18 changes: 18 additions & 0 deletions website/content/docs/_index.md
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
42 changes: 42 additions & 0 deletions website/content/docs/architecture.md
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.
Loading
Loading