diff --git a/.github/workflows/ci.yml b/.gitea/workflows/ci.yml similarity index 100% rename from .github/workflows/ci.yml rename to .gitea/workflows/ci.yml diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..b081667 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,43 @@ +name: release +on: + push: + tags: ['v*'] + workflow_dispatch: + +jobs: + linux: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Linux build deps + run: | + if command -v apt-get >/dev/null; then + sudo apt-get update + sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev + fi + + - name: Setup Flutter (beta) + run: | + git clone https://github.com/flutter/flutter.git -b beta --depth 1 ~/flutter + echo "$HOME/flutter/bin" >> $GITHUB_PATH + + - name: Build tarball + run: tool/package_linux.sh + + - name: Upload tarball artifact + uses: actions/upload-artifact@v4 + with: + name: linux-tarball + path: dist/*.tar.gz + + - name: Publish release + manifest + if: startsWith(github.ref, 'refs/tags/v') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} + run: tool/release_publish.sh "${{ github.ref_name }}" diff --git a/HANDOFF.md b/HANDOFF.md index 55e275b..60ac031 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -287,10 +287,12 @@ remain in session_screen.dart. ETXTBSY forbids writing the running exe, delete+rename is fine). Version stamped via `--dart-define=APP_VERSION` (wired in `tool/package_linux.sh`; dev runs report - `0.0.0-dev`). USER-OWNED PENDING: publish - `updates/manifest.json` + release tarball on Forgejo (schema - in `updates/manifest.example.json`); until then --upgrade - reports failure against the missing manifest. No silent + `0.0.0-dev`). Publishing is CI-driven: tag `vX.Y.Z` → + `.gitea/workflows/release.yml` builds, creates the Forgejo + release, uploads the tarball, commits the manifest back + (`tool/release_publish.sh` does the same by hand). + Manual trigger only: bump desktop pubspec in a release commit + first (script enforces tag == pubspec). No silent proto jumps: updater warns when manifest protocol != local. Environment: dart 3.14 beta, flutter 3.48 beta, mpv+ffmpeg present, ~/Videos/chiyo.mp4 sample. Never kill the user's Syncplay mpv diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a0d879f..cf1d8da 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -24,26 +24,47 @@ desktop 17 + 7 updater. ## Backlog (priority order, each one sitting-sized) -1. Update endpoint live: publish `updates/manifest.json` + first - release tarball on Forgejo (user-owned step; schema in - `updates/manifest.example.json`). Until then `--upgrade` reports - up-to-date against a missing manifest. +1. First real release: release commit bumps desktop pubspec, tag + `v0.2.0`, CI (`release.yml`) builds the tarball, publishes the + Forgejo release, and commits the manifest back. Verifies the + whole `--upgrade` chain end to end. Manual fallback is + `tool/package_linux.sh` + `tool/release_publish.sh `. 2. Jumpy seek: live repro with timeline log first, then fix the seek-land retry vs deadband/debounce fight. -3. Sounds swap-in: replace `NoopSfxService` (flutter_soloud preload, +3. EOF stall (bug, dogfood 09-12): playback stops just before the + end and gates HELD ("waiting for X (3.9s)"). Suspect the + buffered≥4s readiness gate is unsatisfiable near end of media. +4. Tiny buffer (bug, dogfood 09-12): 1–3 s observed; want ~2 min + for a 1.4 GB / 24 min file (intro skip + surviving short + drops). Investigate demuxer forward-queue props + 64 MB + `bufferSize`, then raise. +5. Recovery controls: reload-current-media button + restart-player + button for when playback wedges. +6. Link conveniences: Ctrl+V pastes clipboard into the media + field; session link swaps update the remembered media (today + only the connect screen persists); session shows the open link + with an open-in-browser button. +7. Time UI: remaining time until end + scrubber preview (target + time while scrubbing). +8. Shortcut parity pass with mpv / mpv.net. +9. Sounds swap-in: replace `NoopSfxService` (flutter_soloud preload, Kenney/ObsydianX/akx sources in HANDOFF §7); keep seek/pause/ resume cues muted or near-silent. -4. `/health` auth + stop broadcasting the full media URL (api_key - leaks onto the wire). -5. File-join wart: welcome mediaUrl advertises 127.0.0.1, remote file - joiners fail-open then self-heal on first room broadcast. -6. Cache-exhaustion long outage via `tool/shaper.dart --kill-after`. -7. Phase 5 chaos suite (host drop 5 s / 30 s, viewer stall, 20-seek - storm) + OOM / stuck-room / drift-recovery assertions. -8. Windows zip in `tool/package_linux.sh`'s sibling + Discord card. -9. TLS/Caddy doc. -10. Chat history pane (schema exists, Activity log only for now). -11. Backfill LAN soak + 1.4 GB test details (record-only, user ran +10. `/health` auth + stop broadcasting the full media URL (api_key + leaks onto the wire). +11. File-join wart: welcome mediaUrl advertises 127.0.0.1, remote file + joiners fail-open then self-heal on first room broadcast. +12. Cache-exhaustion long outage via `tool/shaper.dart --kill-after`. +13. Phase 5 chaos suite (host drop 5 s / 30 s, viewer stall, 20-seek + storm) + OOM / stuck-room / drift-recovery assertions. +14. Windows zip in packaging + Discord card. +15. TLS/Caddy doc. +16. Chat history pane (schema exists, Activity log only for now). +17. Shaders: anime4k upscaling via mpv glsl-shaders + UI toggle. +18. Avatar caching + static serving (worth-it decision open): + serve avatar bytes once over HTTP by hash instead of + base64-in-every-room-broadcast. +19. Backfill LAN soak + 1.4 GB test details (record-only, user ran both 09-12). ## Epics (multi-sitting, scheduled whole — not before the backlog diff --git a/tool/release_publish.sh b/tool/release_publish.sh new file mode 100755 index 0000000..283309f --- /dev/null +++ b/tool/release_publish.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Publish a release to Forgejo: create the release, upload the Linux +# tarball built by tool/package_linux.sh, and commit the updated +# updates/manifest.json back to main so --upgrade clients see the new +# version at the stable raw URL. +# Usage: tool/release_publish.sh vX.Y.Z (run after package_linux.sh) +# Env: RELEASE_TOKEN (preferred) or the Actions-provided GITHUB_TOKEN / +# GITEA_TOKEN (needs repo write or the final push is denied — then set a +# RELEASE_TOKEN secret with write access). +set -euo pipefail + +TAG="${1:?usage: tool/release_publish.sh vX.Y.Z}" +VER="${TAG#v}" +[[ "$VER" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "error: tag must look like v0.2.0, got $TAG" >&2; exit 1; } + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TARBALL="$ROOT/dist/watch-together-$VER-linux-x64.tar.gz" +[[ -f "$TARBALL" ]] || { echo "error: run tool/package_linux.sh first ($TARBALL missing)" >&2; exit 1; } + +PUB="$(sed -n 's/^version: \([0-9.]*\).*/\1/p' "$ROOT/apps/desktop/pubspec.yaml")" +[[ "$PUB" == "$VER" ]] || { echo "error: tag $TAG != desktop pubspec $PUB (bump it in a release commit first)" >&2; exit 1; } + +ORIGIN="$(git -C "$ROOT" remote get-url origin)" +SERVER="$(echo "$ORIGIN" | sed -E 's#(https?://[^/]+)/.*#\1#')" +REPO="$(echo "$ORIGIN" | sed -E 's#.*[:/]([^/]+/[^/]+?)(\.git)?$#\1#')" +TOKEN="${RELEASE_TOKEN:-${GITHUB_TOKEN:-${GITEA_TOKEN:-}}}" +[[ -n "$TOKEN" ]] || { echo "error: set RELEASE_TOKEN (or run in Actions with a write token)" >&2; exit 1; } + +PROTO="$(sed -n 's/.*protoVersion = \([0-9]*\).*/\1/p' "$ROOT/packages/core/lib/src/constants.dart")" +SHA="$(sha256sum "$TARBALL" | cut -d' ' -f1)" +ASSET="$(basename "$TARBALL")" +PREV="$(git -C "$ROOT" describe --tags --abbrev=0 HEAD 2>/dev/null || true)" +if [[ -n "$PREV" && "$PREV" != "$TAG" ]]; then + NOTES="$(git -C "$ROOT" log "$PREV..HEAD" --format='- %s' | head -n 20)" +else + NOTES="Watch together $VER." +fi +export REL_TAG="$TAG" REL_VER="$VER" REL_NOTES="$NOTES" + +echo "creating release $TAG on $REPO ..." +REL_ID="$(curl -fsS -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \ + -d "$(python3 -c 'import json,os; print(json.dumps({"tag_name": os.environ["REL_TAG"], "name": os.environ["REL_TAG"], "body": "Watch together " + os.environ["REL_VER"] + "\n\n" + os.environ["REL_NOTES"], "draft": False, "prerelease": False}))')" \ + "$SERVER/api/v1/repos/$REPO/releases" \ + | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')" +echo "release id: $REL_ID" + +echo "uploading $ASSET ..." +curl -fsS -H "Authorization: token $TOKEN" -H 'Content-Type: application/gzip' \ + --data-binary "@$TARBALL" \ + "$SERVER/api/v1/repos/$REPO/releases/$REL_ID/assets?name=$ASSET" > /dev/null + +export MANIFEST_FILE="$ROOT/updates/manifest.json" MANIFEST_VER="$VER" \ + MANIFEST_PROTO="$PROTO" MANIFEST_SHA="$SHA" \ + MANIFEST_URL="$SERVER/$REPO/releases/download/$TAG/$ASSET" +python3 - <<'EOF' +import json, os +path = os.environ['MANIFEST_FILE'] +try: + with open(path) as f: + manifest = json.load(f) +except (FileNotFoundError, ValueError): + manifest = {} +manifest.update({ + 'version': os.environ['MANIFEST_VER'], + 'protocol': int(os.environ['MANIFEST_PROTO']), + 'url': os.environ['MANIFEST_URL'], + 'sha256': os.environ['MANIFEST_SHA'], + 'notes': os.environ['REL_NOTES'].splitlines()[0] if os.environ['REL_NOTES'] else '', +}) +with open(path, 'w') as f: + json.dump(manifest, f, indent=2) + f.write('\n') +EOF + +echo "committing manifest back to main ..." +git -C "$ROOT" fetch origin main 2>/dev/null || true +git -C "$ROOT" checkout -B main origin/main +git -C "$ROOT" add updates/manifest.json +git -C "$ROOT" -c user.name='watch-together-release' -c user.email='release@localhost' \ + commit -m "Release $TAG manifest" -- updates/manifest.json +if ! git -C "$ROOT" push -o ci.skip origin main 2>/dev/null; then + git -C "$ROOT" push origin main +fi +echo "published $TAG — manifest live at updates/manifest.json on main."