Compare commits
3
Commits
15ad9f355c
...
v0.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3e6fc8fa6 | ||
|
|
88780f7701 | ||
|
|
466c49e4b0 |
@@ -42,6 +42,9 @@ clicking an item copies it back to the clipboard.
|
|||||||
code and server URL in `UserDefaults` (`sync.code`, `sync.serverURL`,
|
code and server URL in `UserDefaults` (`sync.code`, `sync.serverURL`,
|
||||||
`sync.version`), pushes the whole store as JSON, and polls every 5s.
|
`sync.version`), pushes the whole store as JSON, and polls every 5s.
|
||||||
- `Server/main.go` — the Go sync backend (single file, `net/http` + `lib/pq`).
|
- `Server/main.go` — the Go sync backend (single file, `net/http` + `lib/pq`).
|
||||||
|
Also ships `Server/README.md` (self-hosting) and `Server/prompt-sync.service`
|
||||||
|
(systemd unit). Logs one line per request, creates its schema on startup, and
|
||||||
|
shuts down gracefully on SIGTERM so a redeploy cannot cut off a push.
|
||||||
|
|
||||||
## Sync protocol
|
## Sync protocol
|
||||||
|
|
||||||
@@ -53,6 +56,7 @@ payload as opaque `JSONB`; it never parses note contents.
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `POST /api/codes` | Generates a unique code and inserts an empty group |
|
| `POST /api/codes` | Generates a unique code and inserts an empty group |
|
||||||
| `GET /api/sync/{code}` | `{"version":N,"updatedAt":…,"data":{…}\|null}`, 404 if unknown |
|
| `GET /api/sync/{code}` | `{"version":N,"updatedAt":…,"data":{…}\|null}`, 404 if unknown |
|
||||||
|
| `GET /api/sync/{code}/version` | `{"version":N}` — the cheap poll |
|
||||||
| `PUT /api/sync/{code}` | Upserts the payload, returns the incremented version |
|
| `PUT /api/sync/{code}` | Upserts the payload, returns the incremented version |
|
||||||
| `GET /healthz` | Liveness |
|
| `GET /healthz` | Liveness |
|
||||||
|
|
||||||
@@ -60,6 +64,9 @@ Client rules:
|
|||||||
|
|
||||||
- Payload = `{sections, activeSectionID, images}`, where `images` maps a
|
- Payload = `{sections, activeSectionID, images}`, where `images` maps a
|
||||||
filename to base64 PNG so photos travel with the notes.
|
filename to base64 PNG so photos travel with the notes.
|
||||||
|
- Polling is two-step: `GET /version` every 5s (tiny) and the full payload only
|
||||||
|
when the version moved. Fetching the payload unconditionally would re-download
|
||||||
|
every base64 photo every 5 seconds.
|
||||||
- Version counter is server-owned; the client stores the last version it saw
|
- Version counter is server-owned; the client stores the last version it saw
|
||||||
in `sync.version` and only applies a remote payload when `remote.version >
|
in `sync.version` and only applies a remote payload when `remote.version >
|
||||||
lastVersion`.
|
lastVersion`.
|
||||||
@@ -105,15 +112,26 @@ Bundle id is `dev.tzgyn.prompt`.
|
|||||||
|
|
||||||
## Release process
|
## Release process
|
||||||
|
|
||||||
1. `./release.sh <version>` — builds, zips `Prompt.app` to `dist/Prompt.zip`,
|
Notarization is **required**: an unnotarized build is not a release. Never
|
||||||
and prints its sha256 (it does not tag or upload). Notarizes when the
|
publish one, and never suggest quarantine workarounds (`xattr -dr
|
||||||
`PROMPT_NOTARY_*` env vars (or the shared Kapture App Store Connect key at
|
com.apple.quarantine`, `brew install --cask --no-quarantine`). `release.sh`
|
||||||
`~/.config/kapture/AuthKey_K5KXMMSB28.p8`) are present; set
|
enforces this — it fails if it cannot notarize, and refuses to print a sha256
|
||||||
`PROMPT_SKIP_NOTARY=1` to skip.
|
unless `spctl` reports `source=Notarized Developer ID`. `PROMPT_SKIP_NOTARY=1`
|
||||||
|
exists only for a local dev build and prints a "NOT FOR PUBLICATION" warning.
|
||||||
|
|
||||||
|
1. `./release.sh <version>` — builds, notarizes, staples, zips `Prompt.app` to
|
||||||
|
`dist/Prompt.zip`, and prints its sha256 (it does not tag or upload). Uses
|
||||||
|
the `PROMPT_NOTARY_*` env vars, defaulting to the shared Kapture App Store
|
||||||
|
Connect key at `~/.config/kapture/AuthKey_K5KXMMSB28.p8`.
|
||||||
2. Commit and push `main`.
|
2. Commit and push `main`.
|
||||||
3. `tea releases create --tag v<version> --asset dist/Prompt.zip --repo TZGyn/prompt --login gitea.tzgyn.dev`.
|
3. `tea releases create --tag v<version> --title "Prompt <version>" --note "<what changed>" --asset dist/Prompt.zip --repo TZGyn/prompt --login gitea.tzgyn.dev`.
|
||||||
4. Bump `version`/`sha256` in `~/.cache/homebrew-tap/Casks/prompt.rb`, push,
|
4. Add/update `~/.cache/homebrew-tap/Casks/prompt.rb` (`version` + `sha256`),
|
||||||
then `git -C /opt/homebrew/Library/Taps/tzgyn/homebrew-tap pull --ff-only`.
|
push the tap, then
|
||||||
|
`git -C /opt/homebrew/Library/Taps/tzgyn/homebrew-tap pull --ff-only` and
|
||||||
|
`brew audit --cask tzgyn/tap/prompt`.
|
||||||
|
5. Verify by downloading the release asset back, comparing the sha256 with the
|
||||||
|
cask, and unzipping with `ditto -x -k` (not `unzip`, which adds `._*` files
|
||||||
|
and breaks the signature check) then `spctl -a -vv Prompt.app`.
|
||||||
|
|
||||||
**Always print the upgrade script after any release or version bump:**
|
**Always print the upgrade script after any release or version bump:**
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ to your work. Click anything to copy it back to your clipboard.
|
|||||||
| Switch/create section | Cmd+K |
|
| Switch/create section | Cmd+K |
|
||||||
| Search | Cmd+F |
|
| Search | Cmd+F |
|
||||||
| Focus composer | Cmd+N |
|
| Focus composer | Cmd+N |
|
||||||
|
| Sync settings | Cmd+, |
|
||||||
| Copy selection as numbered list | Shift+Cmd+C |
|
| Copy selection as numbered list | Shift+Cmd+C |
|
||||||
| Close panel | Esc |
|
| Close panel | Esc |
|
||||||
|
|
||||||
@@ -65,9 +66,11 @@ Notes:
|
|||||||
|
|
||||||
- Both Macs must be able to reach the server. A LAN address or a Tailscale
|
- Both Macs must be able to reach the server. A LAN address or a Tailscale
|
||||||
hostname works; `localhost` only works if the server runs on the same Mac.
|
hostname works; `localhost` only works if the server runs on the same Mac.
|
||||||
|
See [Server/README.md](Server/README.md) to deploy it on a VPS or home server.
|
||||||
- Joining a group that already has notes replaces what's on that Mac.
|
- Joining a group that already has notes replaces what's on that Mac.
|
||||||
- The app polls every 5 seconds and pushes 1.5s after a change; last write
|
- The app polls every 5 seconds (a tiny version check) and pushes 1.5s after a
|
||||||
wins. Changes that came from the server are not echoed back.
|
change; last write wins, and changes that came from the server are not echoed
|
||||||
|
back.
|
||||||
- Verify sync from a terminal with `Prompt --sync-check`.
|
- Verify sync from a terminal with `Prompt --sync-check`.
|
||||||
|
|
||||||
## Build from source
|
## Build from source
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Prompt sync server
|
||||||
|
|
||||||
|
Small Go service that stores one JSON blob per sync group, keyed by an 8-digit
|
||||||
|
code (`1234-5678`). There is no authentication: the code is the shared secret,
|
||||||
|
so anyone who knows it can read and overwrite that group. It is meant to be
|
||||||
|
self-hosted for personal use, not exposed as a public service.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Postgres (any recent version; developed against 18)
|
||||||
|
- A host both Macs can reach — a VPS, a home server, or a Tailscale node
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
createdb prompt_sync
|
||||||
|
cd Server
|
||||||
|
PROMPT_SYNC_DATABASE_URL="postgres://localhost:5432/prompt_sync?sslmode=disable" \
|
||||||
|
go run . # or: go build -o prompt-sync . && ./prompt-sync
|
||||||
|
```
|
||||||
|
|
||||||
|
| Env var | Default |
|
||||||
|
| --- | --- |
|
||||||
|
| `PROMPT_SYNC_ADDR` | `:8080` |
|
||||||
|
| `PROMPT_SYNC_DATABASE_URL` | `postgres://localhost:5432/prompt_sync?sslmode=disable` |
|
||||||
|
|
||||||
|
The schema is created on startup. Every request is logged as
|
||||||
|
`METHOD PATH STATUS DURATION`; SIGTERM/SIGINT shut down gracefully so an
|
||||||
|
in-flight push is not cut off mid-write.
|
||||||
|
|
||||||
|
## Deploy to a Linux host
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# from the repo root, on the Mac
|
||||||
|
cd Server
|
||||||
|
GOOS=linux GOARCH=arm64 go build -o prompt-sync . # amd64 for x86 hosts
|
||||||
|
scp prompt-sync <host>:/tmp/ && ssh <host> 'sudo mv /tmp/prompt-sync /usr/local/bin/'
|
||||||
|
|
||||||
|
# on the host: create the database and role, then
|
||||||
|
sudo cp Server/prompt-sync.service /etc/systemd/system/
|
||||||
|
sudo systemctl edit --full prompt-sync # set PROMPT_SYNC_DATABASE_URL
|
||||||
|
sudo systemctl enable --now prompt-sync
|
||||||
|
journalctl -u prompt-sync -f # verify it is listening
|
||||||
|
```
|
||||||
|
|
||||||
|
Put it behind TLS if it is reachable from the internet — the code is the only
|
||||||
|
secret, and it travels in the URL path. Both plain HTTP and HTTPS work from the
|
||||||
|
app; an HTTP URL is often fine over a LAN or Tailscale.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
| Method | Path | Behaviour |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `POST` | `/api/codes` | Generates a unique code, inserts an empty group |
|
||||||
|
| `GET` | `/api/sync/{code}` | `{"version":N,"updatedAt":…,"data":{…}\|null}`, 404 if unknown |
|
||||||
|
| `GET` | `/api/sync/{code}/version` | `{"version":N}` — cheap poll used by the app |
|
||||||
|
| `PUT` | `/api/sync/{code}` | Upserts the payload, returns the incremented version |
|
||||||
|
| `GET` | `/healthz` | Liveness (200) |
|
||||||
|
|
||||||
|
Payloads are opaque `JSONB`; the server never parses note contents. Bodies are
|
||||||
|
capped at 50 MB, which is generous enough for base64 photos.
|
||||||
|
|
||||||
|
## Backups
|
||||||
|
|
||||||
|
The whole state is one table, so `pg_dump prompt_sync` is a complete backup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pg_dump prompt_sync | gzip > prompt_sync-$(date +%F).sql.gz
|
||||||
|
```
|
||||||
+68
-1
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
// POST /api/codes -> {"code":"1234-5678"} (create group)
|
// POST /api/codes -> {"code":"1234-5678"} (create group)
|
||||||
// GET /api/sync/{code} -> {"version":N,"data":{...}} (404 if unknown)
|
// GET /api/sync/{code} -> {"version":N,"data":{...}} (404 if unknown)
|
||||||
|
// GET /api/sync/{code}/version -> {"version":N} (cheap poll)
|
||||||
// PUT /api/sync/{code} -> {"version":N+1} (upsert payload)
|
// PUT /api/sync/{code} -> {"version":N+1} (upsert payload)
|
||||||
// GET /healthz -> 200
|
// GET /healthz -> 200
|
||||||
//
|
//
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -23,7 +25,9 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
@@ -69,13 +73,55 @@ func main() {
|
|||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("POST /api/codes", s.createCode)
|
mux.HandleFunc("POST /api/codes", s.createCode)
|
||||||
mux.HandleFunc("GET /api/sync/{code}", s.getSync)
|
mux.HandleFunc("GET /api/sync/{code}", s.getSync)
|
||||||
|
mux.HandleFunc("GET /api/sync/{code}/version", s.getVersion)
|
||||||
mux.HandleFunc("PUT /api/sync/{code}", s.putSync)
|
mux.HandleFunc("PUT /api/sync/{code}", s.putSync)
|
||||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: withLogging(mux),
|
||||||
|
ReadHeaderTimeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shut down cleanly so a redeploy (systemd restart, container stop) never
|
||||||
|
// cuts off an in-flight push and leaves a client thinking it failed.
|
||||||
|
stop := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-stop
|
||||||
|
log.Println("shutting down")
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
_ = server.Shutdown(ctx)
|
||||||
|
}()
|
||||||
|
|
||||||
log.Printf("prompt-sync listening on %s", addr)
|
log.Printf("prompt-sync listening on %s", addr)
|
||||||
log.Fatal(http.ListenAndServe(addr, mux))
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatalf("serve: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// withLogging keeps a one-line trail of method, path, status and duration so a
|
||||||
|
// self-hosted deployment can be debugged from journalctl or docker logs.
|
||||||
|
func withLogging(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
started := time.Now()
|
||||||
|
recorder := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
|
||||||
|
next.ServeHTTP(recorder, r)
|
||||||
|
log.Printf("%s %s %d %s", r.Method, r.URL.Path, recorder.status, time.Since(started).Round(time.Millisecond))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type statusRecorder struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *statusRecorder) WriteHeader(status int) {
|
||||||
|
r.status = status
|
||||||
|
r.ResponseWriter.WriteHeader(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// createCode generates a unique 8-digit code, formats it as 1234-5678, and
|
// createCode generates a unique 8-digit code, formats it as 1234-5678, and
|
||||||
@@ -136,6 +182,27 @@ func (s *Server) getSync(w http.ResponseWriter, r *http.Request) {
|
|||||||
version, updatedAt.UTC().Format(time.RFC3339), data)
|
version, updatedAt.UTC().Format(time.RFC3339), data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getVersion is the cheap poll: clients ask for the version every few seconds
|
||||||
|
// and only download the payload (which can carry base64 images) when it moved.
|
||||||
|
func (s *Server) getVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
|
code := r.PathValue("code")
|
||||||
|
if !codePattern.MatchString(code) {
|
||||||
|
writeError(w, http.StatusBadRequest, "code must look like 1234-5678")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var version int64
|
||||||
|
err := s.db.QueryRow(`SELECT version FROM sync_groups WHERE code = $1`, code).Scan(&version)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
writeError(w, http.StatusNotFound, "unknown code")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "database error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]int64{"version": version})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) putSync(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) putSync(w http.ResponseWriter, r *http.Request) {
|
||||||
code := r.PathValue("code")
|
code := r.PathValue("code")
|
||||||
if !codePattern.MatchString(code) {
|
if !codePattern.MatchString(code) {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Prompt sync server
|
||||||
|
Documentation=https://gitea.tzgyn.dev/TZGyn/prompt
|
||||||
|
After=network-online.target postgresql.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
# The binary is built on a Mac with:
|
||||||
|
# GOOS=linux GOARCH=arm64 go build -o prompt-sync .
|
||||||
|
ExecStart=/usr/local/bin/prompt-sync
|
||||||
|
Environment=PROMPT_SYNC_ADDR=:8080
|
||||||
|
Environment=PROMPT_SYNC_DATABASE_URL=postgres://prompt:CHANGE_ME@localhost:5432/prompt_sync?sslmode=disable
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=2
|
||||||
|
# The server only needs to talk to Postgres and bind :8080.
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -495,6 +495,10 @@ struct PanelView: View {
|
|||||||
.keyboardShortcut("k", modifiers: .command)
|
.keyboardShortcut("k", modifiers: .command)
|
||||||
Button("") { focusedField = .composer }
|
Button("") { focusedField = .composer }
|
||||||
.keyboardShortcut("n", modifiers: .command)
|
.keyboardShortcut("n", modifiers: .command)
|
||||||
|
Button("") {
|
||||||
|
withAnimation(.easeInOut(duration: 0.15)) { showSync.toggle() }
|
||||||
|
}
|
||||||
|
.keyboardShortcut(",", modifiers: .command)
|
||||||
Button("") {
|
Button("") {
|
||||||
guard !selection.isEmpty else { return }
|
guard !selection.isEmpty else { return }
|
||||||
store.copyAsList(ids: selection)
|
store.copyAsList(ids: selection)
|
||||||
|
|||||||
@@ -181,22 +181,34 @@ final class SyncService: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func poll() {
|
private func poll() {
|
||||||
guard !isSyncing, let code, let url = endpoint("/api/sync/\(code)") else { return }
|
guard !isSyncing, let code, let versionURL = endpoint("/api/sync/\(code)/version") else { return }
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
let (data, response) = try await URLSession.shared.data(from: url)
|
// Cheap poll first: the full payload can be megabytes of
|
||||||
if (response as? HTTPURLResponse)?.statusCode == 404 {
|
// base64 images, so only download it when the version moved.
|
||||||
|
let (versionData, versionResponse) = try await URLSession.shared.data(from: versionURL)
|
||||||
|
if (versionResponse as? HTTPURLResponse)?.statusCode == 404 {
|
||||||
lastError = "Sync group no longer exists on the server"
|
lastError = "Sync group no longer exists on the server"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
|
guard let versionHTTP = versionResponse as? HTTPURLResponse, versionHTTP.statusCode == 200 else {
|
||||||
syncLog("poll HTTP \((response as? HTTPURLResponse)?.statusCode ?? -1)")
|
syncLog("poll HTTP \((versionResponse as? HTTPURLResponse)?.statusCode ?? -1)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let remoteVersion = try JSONDecoder().decode([String: Int64].self, from: versionData)["version"] ?? 0
|
||||||
|
guard remoteVersion > lastVersion else {
|
||||||
|
lastError = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
syncLog("poll remote v\(remoteVersion) local v\(lastVersion) — fetching payload")
|
||||||
|
guard let payloadURL = endpoint("/api/sync/\(code)") else { return }
|
||||||
|
let (data, response) = try await URLSession.shared.data(from: payloadURL)
|
||||||
|
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return }
|
||||||
let remote = try JSONDecoder().decode(SyncGetResponse.self, from: data)
|
let remote = try JSONDecoder().decode(SyncGetResponse.self, from: data)
|
||||||
syncLog("poll remote v\(remote.version) local v\(lastVersion)")
|
if let payload = remote.data {
|
||||||
if remote.version > lastVersion, let payload = remote.data {
|
|
||||||
apply(payload, version: remote.version)
|
apply(payload, version: remote.version)
|
||||||
|
} else {
|
||||||
|
lastVersion = remote.version
|
||||||
}
|
}
|
||||||
lastError = nil
|
lastError = nil
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
+16
-8
@@ -23,7 +23,8 @@ ditto -c -k --keepParent Prompt.app dist/Prompt.zip
|
|||||||
|
|
||||||
if [ -n "${PROMPT_SKIP_NOTARY:-}" ]; then
|
if [ -n "${PROMPT_SKIP_NOTARY:-}" ]; then
|
||||||
echo
|
echo
|
||||||
echo "Skipping notarization (PROMPT_SKIP_NOTARY set)."
|
echo "!! NOTARIZATION SKIPPED — this zip is NOT FOR PUBLICATION."
|
||||||
|
echo "!! It is signed but Gatekeeper will reject it on other Macs."
|
||||||
shasum -a 256 dist/Prompt.zip
|
shasum -a 256 dist/Prompt.zip
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -34,15 +35,16 @@ PROMPT_NOTARY_API_KEY_ID="${PROMPT_NOTARY_API_KEY_ID:-K5KXMMSB28}"
|
|||||||
PROMPT_NOTARY_API_ISSUER="${PROMPT_NOTARY_API_ISSUER:-3e25e5dc-8b75-453f-a71c-c7f3feaac628}"
|
PROMPT_NOTARY_API_ISSUER="${PROMPT_NOTARY_API_ISSUER:-3e25e5dc-8b75-453f-a71c-c7f3feaac628}"
|
||||||
|
|
||||||
if [ ! -f "$PROMPT_NOTARY_API_KEY_PATH" ]; then
|
if [ ! -f "$PROMPT_NOTARY_API_KEY_PATH" ]; then
|
||||||
echo "Notarization skipped — key not found at $PROMPT_NOTARY_API_KEY_PATH." >&2
|
echo "ERROR: notarization key not found at $PROMPT_NOTARY_API_KEY_PATH." >&2
|
||||||
shasum -a 256 dist/Prompt.zip
|
echo "Notarization is required for a release; set PROMPT_SKIP_NOTARY=1 only" >&2
|
||||||
exit 0
|
echo "for a local dev build that must not be published." >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! command -v xcrun >/dev/null 2>&1 || ! xcrun --find notarytool >/dev/null 2>&1; then
|
if ! command -v xcrun >/dev/null 2>&1 || ! xcrun --find notarytool >/dev/null 2>&1; then
|
||||||
echo "Notarization skipped — xcrun notarytool not found (requires Xcode 13+)." >&2
|
echo "ERROR: xcrun notarytool not found (requires Xcode 13+)." >&2
|
||||||
shasum -a 256 dist/Prompt.zip
|
echo "Notarization is required for a release." >&2
|
||||||
exit 0
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Submitting to Apple for notarization (first run can take minutes)..."
|
echo "Submitting to Apple for notarization (first run can take minutes)..."
|
||||||
@@ -69,6 +71,12 @@ xcrun stapler validate Prompt.app
|
|||||||
rm -f dist/Prompt.zip
|
rm -f dist/Prompt.zip
|
||||||
ditto -c -k --keepParent Prompt.app dist/Prompt.zip
|
ditto -c -k --keepParent Prompt.app dist/Prompt.zip
|
||||||
|
|
||||||
|
if ! spctl -a -vv Prompt.app 2>&1 | grep -q "source=Notarized Developer ID"; then
|
||||||
|
echo "ERROR: spctl does not report a notarized build — do not publish this." >&2
|
||||||
|
spctl -a -vv Prompt.app >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Done — notarized."
|
echo "Done — notarized and stapled."
|
||||||
shasum -a 256 dist/Prompt.zip
|
shasum -a 256 dist/Prompt.zip
|
||||||
|
|||||||
Reference in New Issue
Block a user