Skip to content
Merged
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
12 changes: 6 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ CODEX_TOKEN=

# --- OpenCode Go Configuration ---
# Track OpenCode Go subscription quotas. Full walkthrough: docs/OPENCODE_SETUP.md
# Recommended: a console service-account key with usage read access (oc_sk_...).
# onWatch reads the plan's own 5-hour/weekly/monthly meters. No browser cookie needed.
# onWatch reads the plan's own 5-hour/weekly/monthly meters from the console API.
# Option 1: a console service-account key with usage read access (oc_sk_...).
OPENCODE_GO_API_KEY=
# Legacy fallback (dashboard scrape) — used only when OPENCODE_GO_API_KEY is empty.
# Both values are required for scrape mode.
# Workspace ID from https://opencode.ai/workspace/wrk_.../go
# Option 2 (used when OPENCODE_GO_API_KEY is empty): your browser session.
# Both values are required.
# Workspace ID (wrk_...), sent as the x-org-id header
OPENCODE_GO_WORKSPACE_ID=
# Browser cookie value named "auth" from opencode.ai (value only, no auth= prefix)
# Value of the __Host-console_session cookie from opencode.ai (the old "auth" cookie no longer works)
OPENCODE_GO_AUTH_COOKIE=

# --- GitHub Copilot Configuration (Beta) ---
Expand Down
291 changes: 231 additions & 60 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
name: CI

# Every job runs in parallel on its own runner, so wall time is the slowest
# job, not the sum. "CI OK" at the bottom depends on all of them and is the
# single check to require on main.

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

# A new push to the same PR cancels the older, now-stale run.
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
test:
lint:
runs-on: ubuntu-latest
name: Test
name: Lint
timeout-minutes: 15

steps:
- uses: actions/checkout@v4
Expand All @@ -20,15 +30,69 @@ jobs:
with:
go-version-file: go.mod

- name: Lint
- name: gofmt
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "These files need gofmt:"
echo "$unformatted"
exit 1
fi

# Vet every shipped OS and build-tag combination from one runner, so a
# file that only compiles on one platform cannot slip through.
- name: go vet (all platforms)
run: |
go fmt ./...
go vet ./...
for os in linux darwin windows; do
echo "== GOOS=$os"
GOOS=$os go vet ./...
done
GOOS=linux go vet -tags menubar ./...
GOOS=windows go vet -tags menubar ./...

test:
name: Test (${{ matrix.name }})
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- name: Linux
os: ubuntu-latest
race: "-race"
tags: menubar
- name: macOS
os: macos-15
race: "-race"
tags: menubar
# The race detector needs cgo, which the Windows runner lacks.
- name: Windows
os: windows-latest
race: ""
tags: menubar

steps:
- uses: actions/checkout@v4

- name: Test with coverage
run: go test -race -coverprofile=coverage.out -covermode=atomic -count=1 ./...
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Test
shell: bash
run: go test ${{ matrix.race }} -timeout 15m -coverprofile=coverage.out -covermode=atomic -count=1 ./...

# The tray companion is compiled only with -tags menubar.
- name: Test tray packages
shell: bash
env:
CGO_LDFLAGS: ${{ runner.os == 'macOS' && '-framework UniformTypeIdentifiers' || '' }}
run: go test ${{ matrix.race }} -timeout 15m -tags ${{ matrix.tags }} -count=1 ./internal/menubar ./internal/web ./cmd/onwatch

- name: Upload coverage to Codecov
if: runner.os == 'Linux'
uses: codecov/codecov-action@v4
with:
files: ./coverage.out
Expand All @@ -37,12 +101,45 @@ jobs:
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

- name: Build
run: go build -o onwatch ./cmd/onwatch

tray-linux:
# Alpine is what the shell Docker image runs on: musl libc and busybox
# instead of glibc and coreutils.
test-alpine:
runs-on: ubuntu-latest
name: Tray Linux
name: Test (Alpine)
timeout-minutes: 30
container: golang:1.25-alpine

steps:
- uses: actions/checkout@v4

# Test the musl/busybox userland as a normal host: onWatch switches to
# its Docker defaults (/data, foreground only) when /.dockerenv exists,
# and root bypasses the permission checks some tests rely on.
- name: Prepare non-root host
run: |
rm -f /.dockerenv
adduser -D tester
chown -R tester "$GITHUB_WORKSPACE"

- name: Test
run: su tester -s /bin/sh -c "export PATH=/usr/local/go/bin:\$PATH; cd '$GITHUB_WORKSPACE' && go test -timeout 15m -count=1 ./..."

# Build every release artifact exactly as release.yml does, so a PR cannot
# break the release pipeline.
build:
name: Build ${{ matrix.goos }}/${{ matrix.goarch }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-latest, goos: linux, goarch: amd64, cgo: "0", tags: menubar }
- { os: ubuntu-latest, goos: linux, goarch: arm64, cgo: "0", tags: menubar }
- { os: ubuntu-latest, goos: windows, goarch: amd64, cgo: "0", tags: menubar }
- { os: ubuntu-latest, goos: windows, goarch: arm64, cgo: "0", tags: menubar }
- { os: macos-15, goos: darwin, goarch: amd64, cgo: "1", tags: "menubar,desktop,production" }
- { os: macos-15, goos: darwin, goarch: arm64, cgo: "1", tags: "menubar,desktop,production" }

steps:
- uses: actions/checkout@v4
Expand All @@ -52,24 +149,24 @@ jobs:
with:
go-version-file: go.mod

- name: Test tagged tray packages
# -race needs cgo for the test binary; the tray itself stays pure Go,
# which the CGO_ENABLED=0 cross-compile step below proves.
run: |
go vet -tags menubar ./...
go test -race -tags menubar -count=1 ./internal/menubar ./internal/web ./cmd/onwatch

- name: Cross-compile tray binaries
- name: Build
env:
CGO_ENABLED: "0"
run: |
GOOS=linux GOARCH=amd64 go build -tags menubar -o /tmp/onwatch-linux-amd64 ./cmd/onwatch
GOOS=linux GOARCH=arm64 go build -tags menubar -o /tmp/onwatch-linux-arm64 ./cmd/onwatch
GOOS=windows GOARCH=amd64 go build -tags menubar -o /tmp/onwatch-windows-amd64.exe ./cmd/onwatch
CGO_ENABLED: ${{ matrix.cgo }}
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: go build -tags ${{ matrix.tags }} -ldflags="-s -w" -o onwatch-${{ matrix.goos }}-${{ matrix.goarch }} ./cmd/onwatch

tray-windows:
runs-on: windows-latest
name: Tray Windows
e2e:
name: E2E (${{ matrix.name }})
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- { name: Linux, os: ubuntu-latest, tags: menubar }
- { name: macOS, os: macos-15, tags: "menubar,desktop,production" }
- { name: Windows, os: windows-latest, tags: menubar }

steps:
- uses: actions/checkout@v4
Expand All @@ -79,64 +176,138 @@ jobs:
with:
go-version-file: go.mod

- name: Test tagged tray packages
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install E2E dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r tests/e2e/requirements.txt
python -m playwright install --with-deps chromium

- name: Run E2E suite
shell: bash
env:
CGO_ENABLED: "0"
CGO_LDFLAGS: ${{ runner.os == 'macOS' && '-framework UniformTypeIdentifiers' || '' }}
ONWATCH_E2E_GO_BUILD_TAGS: ${{ matrix.tags }}
run: |
go test -tags menubar -count=1 ./internal/menubar
go build -tags menubar -o onwatch-tray.exe ./cmd/onwatch
cd tests/e2e
pytest -v --tracing retain-on-failure --output test-results

- name: Show onWatch logs
if: failure()
shell: bash
run: |
python - <<'EOF'
import glob, os, tempfile
tmp = tempfile.gettempdir()
# Daemon stdout, plus the log file it writes next to its database.
paths = glob.glob(os.path.join(tmp, "onwatch-e2e-*.log")) + glob.glob(os.path.join(tmp, ".onwatch-test.log"))
for path in sorted(paths):
print(f"===== {path}")
with open(path, errors="replace") as f:
print("".join(f.readlines()[-150:]))
EOF

- name: Upload Playwright traces
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-traces-${{ matrix.name }}
path: tests/e2e/test-results
if-no-files-found: ignore

installer-windows:
runs-on: windows-latest
name: Installer Windows
installer:
name: Installer (${{ matrix.name }})
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- { name: Linux, os: ubuntu-latest }
- { name: macOS, os: macos-15 }
- { name: Windows, os: windows-latest }

steps:
- uses: actions/checkout@v4

- name: Test install.sh
if: runner.os != 'Windows'
run: bash tests/test_install.sh

# 5.1 is what ships with Windows and what `irm ... | iex` runs, and it is
# the host where redirected native stderr under ErrorActionPreference=Stop
# becomes a terminating NativeCommandError. 7 keeps the script honest for
# anyone who upgraded.
- name: Test install.ps1 under Windows PowerShell 5.1
if: runner.os == 'Windows'
shell: powershell
run: .\tests\test_install_ps1.ps1

- name: Test install.ps1 under PowerShell 7
if: runner.os == 'Windows'
shell: pwsh
run: .\tests\test_install_ps1.ps1

menubar-macos:
runs-on: macos-15
name: Menubar macOS
docker:
runs-on: ubuntu-latest
name: Docker
timeout-minutes: 20

steps:
- uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build images
run: |
docker build --target runtime-shell -t onwatch:ci-shell .
docker build --target runtime -t onwatch:ci .

- name: Compile tagged menubar packages
# Start each image and wait for the dashboard. Z.ai points at a closed
# local port, so the container makes no outbound provider calls.
- name: Smoke test images
run: |
go test -tags menubar ./internal/menubar ./internal/web
CGO_LDFLAGS="-framework UniformTypeIdentifiers" go build -tags menubar,desktop,production -o /tmp/onwatch-menubar ./cmd/onwatch
for image in onwatch:ci-shell onwatch:ci; do
name=smoke-${image//[:.]/-}
docker run -d --name "$name" -p 19300:9211 \
-e ONWATCH_ADMIN_PASS=ci-smoke -e ZAI_API_KEY=ci -e ZAI_BASE_URL=http://127.0.0.1:1 \
"$image"
ok=""
for i in $(seq 1 30); do
if curl -fsS -o /dev/null http://localhost:19300/login; then ok=1; break; fi
sleep 1
done
docker logs "$name" | tail -40
docker rm -f "$name"
if [ -z "$ok" ]; then echo "$image did not serve /login"; exit 1; fi
done

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
# Catches flake.nix drift such as a stale vendorHash after go.sum changes.
nix:
runs-on: ubuntu-latest
name: Nix
timeout-minutes: 30

- name: Install E2E dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r tests/e2e/requirements.txt
python -m playwright install chromium
steps:
- uses: actions/checkout@v4

- name: Run menubar browser tests
env:
CGO_LDFLAGS: -framework UniformTypeIdentifiers
ONWATCH_E2E_GO_BUILD_TAGS: menubar,desktop,production
- uses: DeterminateSystems/nix-installer-action@v16

- name: Build
run: nix build .#onwatch --print-build-logs

ci-ok:
name: CI OK
if: always()
needs: [lint, test, test-alpine, build, e2e, installer, docker, nix]
runs-on: ubuntu-latest
steps:
- name: All jobs passed
run: |
cd tests/e2e
pytest tests/test_menubar.py -v
if [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') }}" = "true" ]; then
echo "A required job did not pass:"
echo '${{ toJSON(needs) }}'
exit 1
fi
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,4 @@ onwatch-test

# Agent worktrees, local scratch state
.claude/worktrees/
tests/e2e/test-results/
Loading
Loading