Skip to content

Commit 58da1d4

Browse files
committed
chore: add release docs, CI hardening, and telemetry worker
1 parent ef45c07 commit 58da1d4

13 files changed

Lines changed: 257 additions & 8 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ jobs:
4848
key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
4949
restore-keys: cargo-${{ runner.os }}-
5050

51+
- name: Clippy
52+
run: cargo clippy --workspace --all-targets -- -D warnings
53+
5154
- name: Test
5255
run: cargo test --workspace
5356

.github/workflows/gpui-ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
- name: Install jj (for component test fixtures)
4848
env:
4949
# Keep aligned with jj-lib pin in Cargo.toml.
50-
JJ_VERSION: v0.40.0
50+
JJ_VERSION: v0.42.0
5151
run: |
5252
curl -fsSL "https://github.com/jj-vcs/jj/releases/download/${JJ_VERSION}/jj-${JJ_VERSION}-x86_64-unknown-linux-musl.tar.gz" \
5353
| tar -xz -C /tmp ./jj
@@ -86,7 +86,7 @@ jobs:
8686
shell: pwsh
8787
env:
8888
# Keep aligned with jj-lib pin in Cargo.toml.
89-
JJ_VERSION: v0.40.0
89+
JJ_VERSION: v0.42.0
9090
run: |
9191
$url = "https://github.com/jj-vcs/jj/releases/download/$env:JJ_VERSION/jj-$env:JJ_VERSION-x86_64-pc-windows-msvc.zip"
9292
Invoke-WebRequest -Uri $url -OutFile jj.zip

Roadmap.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@ JayJay already covers the common jj history, diff, bookmark, conflict, Git, revi
66

77
- [ ] GPUI write actions: AI commit messages, `jj new`, abandon, squash into parent, file-level split from review marks, bookmark create/move/push, and undo.
88
- [ ] GPUI rewrite and resolve flows: drag-to-rebase insert before/after, subtree movement previews, clearer descendant behavior, and basic `jj resolve` UI.
9-
- [ ] GPUI Linux/Windows polish: desktop entry, hicolor icons, notifications, file picker fallback, and packaging.
10-
- [ ] Diff edit polish: select all, clear all, unsupported-file messaging, and better topology copy.
9+
- [ ] Diff edit polish: unsupported-file messaging, and better topology copy.
10+
- [ ] Stacked PR assistant: detect a jj change stack, assign/push per-change bookmarks, create GitHub PRs bottom-up with dependent bases, and review each layer with bookmark diff.
1111
- [ ] Saved revsets: named revset library plus "save this revset".
1212
- [ ] Evolog polish: inline restore, hide snapshots, and collapse snapshot runs.
13-
- [ ] Tag UI once jj stabilizes the tag command surface.
14-
- [ ] Workspace model: multi-repo tabs or richer workspace switching.
13+
- [ ] GPUI Linux/Windows polish: desktop entry, hicolor icons, notifications, file picker fallback, and packaging.
1514
- [ ] Semantic diff: tree-sitter AST diffing and function-level summaries.
16-
- [ ] ACP integration: let compatible agents drive jj operations through structured JayJay tool calls.
1715

1816
## Known Issues
1917

agents/release.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ Releases are not complete after `just release`. The full release flow is:
1414
8. Push `main` only after `just shell::publish` succeeds, so `docs/appcast.xml` never points at a missing or draft-only asset.
1515
9. Commit and push the Homebrew tap change in `../tap`.
1616

17+
The Cloudflare worker is transparent to appcast generation: `docs/appcast.xml` remains the source of truth, and the worker only proxies it for users who opt into anonymous stats. If a release changes `infra/worker`, the worker name, or any `workers.dev` endpoint, deploy it before shipping and verify both routes:
18+
19+
```bash
20+
cd infra/worker
21+
wrangler deploy
22+
curl -fsS https://jayjay.hewigovens.workers.dev/appcast.xml >/dev/null
23+
curl -fsS 'https://jayjay.hewigovens.workers.dev/ping?platform=gpui&app=jayjay&version=test&os=darwin&arch=arm64'
24+
```
25+
1726
## Required Outputs
1827

1928
- `just release` produces the notarized zip in `build/release/`.

crates/jayjay-core/tests/revsets.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,36 @@ fn immutable_heads_revset_alias_is_available_in_app_parser() {
6565
"expected immutable_heads() expression to parse alongside trunk()"
6666
);
6767
}
68+
69+
#[test]
70+
fn default_revset_evaluates_in_cli_and_app_parser() {
71+
let temp_dir = init_jj_repo();
72+
let repo_path = temp_dir.path().join("repo");
73+
let repo_str = repo_path.to_str().expect("repo path utf-8");
74+
75+
let cli = run_jj(&[
76+
"-R",
77+
repo_str,
78+
"log",
79+
"--no-graph",
80+
"-r",
81+
DEFAULT_REVSET,
82+
"-T",
83+
"commit_id.short() ++ \"\\n\"",
84+
]);
85+
assert!(
86+
!cli.stdout.is_empty(),
87+
"jj CLI should evaluate JayJay's default revset"
88+
);
89+
90+
let repo = Repo::open(&repo_path).expect("open repo");
91+
let app = repo.log(DEFAULT_REVSET).expect("evaluate default revset");
92+
assert!(
93+
!app.is_empty(),
94+
"JayJay should evaluate the same default revset as the jj CLI"
95+
);
96+
}
97+
6898
#[test]
6999
fn custom_immutable_heads_alias_can_reference_builtin_default_alias() {
70100
let temp_dir = init_jj_repo();

infra/worker/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
build/
2+
.wrangler/
3+
*.log
4+
.dev.vars

infra/worker/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# JayJay worker (Go)
2+
3+
A single Cloudflare Worker (standard Go → WASM via [`syumai/workers`](https://github.com/syumai/workers)) for JayJay service endpoints. Current routes:
4+
5+
- **`GET /appcast.xml`** — macOS/Sparkle opt-in proxy. Logs aggregate request stats, then proxies the real appcast from `APPCAST_ORIGIN`. The appcast's EdDSA signature is verified in-app, so the proxy can't tamper with updates.
6+
- **`GET /ping`** — GPUI (Linux/Windows) opt-in daily ping. Logs app version + OS + arch. Returns `200 ok`.
7+
8+
Stats land in **D1** (SQLite); schema in `schema.sql`. Standard Go, no TinyGo, no wasm-bindgen. Built wasm is ~1.7 MB gzipped — under the 3 MB free-plan limit.
9+
10+
## Privacy
11+
12+
No IP or personal data is stored. The daily-unique counter is a salted SHA-256 of `(IP + day + HASH_SECRET)`, stored only as `unique_key`; the raw IP never leaves the request handler. Telemetry is opt-in (GPUI: `telemetry.enabled = true`; macOS: the "Send anonymous usage stats" Settings toggle, which routes update checks through the appcast proxy).
13+
14+
## Build & deploy
15+
16+
Requires Go 1.24+ and wrangler. The Go assets/shim and wasm are produced by the `[build]` command in `wrangler.toml` (no npm app code):
17+
18+
```bash
19+
wrangler d1 create jayjay_stats # one-time; id already in wrangler.toml
20+
wrangler d1 execute jayjay_stats --remote --file=schema.sql
21+
wrangler secret put HASH_SECRET # one-time; any long random string
22+
wrangler deploy # runs workers-assets-gen + go build, then uploads
23+
```
24+
25+
Local build only: `go run github.com/syumai/workers/cmd/workers-assets-gen -mode=go && GOOS=js GOARCH=wasm go build -o ./build/app.wasm .`
26+
27+
## Query
28+
29+
```bash
30+
wrangler d1 execute jayjay_stats --remote --command "
31+
SELECT day, channel, os, arch, version, COUNT(DISTINCT unique_key) AS dau
32+
FROM pings GROUP BY day, channel, os, arch, version ORDER BY day DESC LIMIT 50"
33+
```

infra/worker/go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module github.com/hewigovens/jayjay/infra/worker
2+
3+
go 1.26.4
4+
5+
require github.com/syumai/workers v0.33.0

infra/worker/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
github.com/syumai/workers v0.33.0 h1:ku24TZT8m32leS5wYId1F+pGPVXhl+TPnxaDwbr66y4=
2+
github.com/syumai/workers v0.33.0/go.mod h1:ZnqmdiHNBrbxOLrZ/HJ5jzHy6af9cmiNZk10R9NrIEA=

infra/worker/main.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// JayJay service worker (Cloudflare Worker, standard Go -> WASM).
2+
//
3+
// GET /appcast.xml — macOS/Sparkle: log the profiling query params Sparkle
4+
// appends, then proxy the real appcast (APPCAST_ORIGIN).
5+
// The EdDSA signature is verified in-app, so the proxy
6+
// cannot tamper with updates.
7+
// GET /ping — GPUI (Linux/Windows): log app version + OS + arch.
8+
//
9+
// Privacy: no IP or personal data is stored. The daily-unique key is a salted
10+
// SHA-256 of (IP + day + HASH_SECRET); the raw IP never leaves this function.
11+
package main
12+
13+
import (
14+
"crypto/sha256"
15+
"database/sql"
16+
"encoding/hex"
17+
"io"
18+
"net/http"
19+
"strconv"
20+
"time"
21+
22+
"github.com/syumai/workers"
23+
"github.com/syumai/workers/cloudflare"
24+
_ "github.com/syumai/workers/cloudflare/d1" // registers the "d1" sql driver
25+
"github.com/syumai/workers/cloudflare/fetch"
26+
)
27+
28+
func main() {
29+
db, err := sql.Open("d1", "DB")
30+
if err != nil {
31+
panic(err)
32+
}
33+
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
34+
logEvent(db, r, "gpui")
35+
w.Write([]byte("ok"))
36+
})
37+
appcastHandler := func(w http.ResponseWriter, r *http.Request) {
38+
logEvent(db, r, "swiftui")
39+
proxyAppcast(w, r)
40+
}
41+
http.HandleFunc("/appcast.xml", appcastHandler)
42+
http.HandleFunc("/", appcastHandler)
43+
workers.Serve(nil) // use http.DefaultServeMux
44+
}
45+
46+
func proxyAppcast(w http.ResponseWriter, r *http.Request) {
47+
req, err := fetch.NewRequest(r.Context(), http.MethodGet, cloudflare.Getenv("APPCAST_ORIGIN"), nil)
48+
if err != nil {
49+
w.WriteHeader(http.StatusInternalServerError)
50+
return
51+
}
52+
res, err := fetch.NewClient().Do(req, nil)
53+
if err != nil {
54+
w.WriteHeader(http.StatusBadGateway)
55+
return
56+
}
57+
defer res.Body.Close()
58+
w.Header().Set("content-type", "application/xml; charset=utf-8")
59+
io.Copy(w, res.Body)
60+
}
61+
62+
func logEvent(db *sql.DB, r *http.Request, channel string) {
63+
day := time.Now().Unix() / 86400
64+
unique := dailyUnique(r.Header.Get("CF-Connecting-IP"), day, cloudflare.Getenv("HASH_SECRET"))
65+
66+
q := r.URL.Query()
67+
version := firstNonEmpty(q.Get("version"), q.Get("appVersionShort"), q.Get("appVersion"))
68+
osName := orDefault(q.Get("os"), appcastDefault(channel, "macos"))
69+
osVersion := firstNonEmpty(q.Get("osver"), q.Get("osVersion"))
70+
arch := q.Get("arch")
71+
if arch == "" {
72+
arch = archFromCPUType(q.Get("cputype"))
73+
}
74+
platform := orDefault(q.Get("platform"), appcastDefault(channel, "macos"))
75+
76+
// Fire-and-forget: a logging failure must never break the update check.
77+
_, _ = db.ExecContext(r.Context(),
78+
`INSERT INTO pings (day, unique_key, channel, platform, os, os_version, arch, version, model)
79+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
80+
day, unique, channel, platform, osName, osVersion, arch, version, q.Get("model"))
81+
}
82+
83+
func dailyUnique(ip string, day int64, secret string) string {
84+
h := sha256.Sum256([]byte(ip + "|" + strconv.FormatInt(day, 10) + "|" + secret))
85+
return hex.EncodeToString(h[:12])
86+
}
87+
88+
// Sparkle sends a mach-o cputype; map the common ones, pass anything else through.
89+
func archFromCPUType(cputype string) string {
90+
switch cputype {
91+
case "16777223":
92+
return "x86_64"
93+
case "16777228":
94+
return "arm64"
95+
case "7":
96+
return "x86"
97+
default:
98+
return cputype
99+
}
100+
}
101+
102+
func firstNonEmpty(vals ...string) string {
103+
for _, v := range vals {
104+
if v != "" {
105+
return v
106+
}
107+
}
108+
return ""
109+
}
110+
111+
func orDefault(v, fallback string) string {
112+
if v == "" {
113+
return fallback
114+
}
115+
return v
116+
}
117+
118+
func appcastDefault(channel, value string) string {
119+
if channel == "swiftui" {
120+
return value
121+
}
122+
return ""
123+
}

0 commit comments

Comments
 (0)