diff --git a/README.md b/README.md index 3569ad90..607f484f 100644 --- a/README.md +++ b/README.md @@ -254,7 +254,7 @@ To try your changes in the app, run `npm run app:build`. It makes an unsigned de Orgo's releases are built from a version tag of this repository by a workflow in Orgo's private repository, which holds the signing and notarization secrets. It builds, signs and notarizes on a GitHub Mac, checks that Gatekeeper accepts the app, and attaches the DMG and zip to the release here with that version's notes from `docs/releases/`. This repository holds no secrets. -Bops updates itself. `scripts/download-publish.sh` puts the release's DMG and its zip (`Bops--arm64-mac.zip`) on bops.bot and writes `/download/latest.json`, which the app checks at launch and every hour. Next to `version` and `notes`, that file has the keys Electron's updater reads, `currentRelease` and `releases` (each with `updateTo.url`, the zip). A newer version downloads in the background, unless the user turned off **Settings → This Mac → Download updates automatically**, and installs when the user clicks Restart or quits Bops. macOS installs it only when the zip's app is signed by the same team (A47GG9FV27) with the same bundle id (`ai.orgo.bops`), so `release.sh` checks the zip for that. A copy opened from the DMG or Downloads, or in a folder the user can't write, can't replace itself and sends the user to bops.bot instead. Versions 0.0.28 and older don't update themselves: their users download the next version from bops.bot once. To try an update on your Mac, run a build with `BOPS_UPDATE_FEED=http://127.0.0.1:/latest.json` (only this Mac's addresses are taken), from a local server holding that file and the zip. +Bops updates itself. With each release, Orgo's `download-publish.sh` (in the same private repository) puts the release's DMG and its zip (`Bops--arm64-mac.zip`) on bops.bot and writes `/download/latest.json`, which the app checks at launch and every hour. Next to `version` and `notes`, that file has the keys Electron's updater reads, `currentRelease` and `releases` (each with `updateTo.url`, the zip). A newer version downloads in the background, unless the user turned off **Settings → This Mac → Download updates automatically**, and installs when the user clicks Restart or quits Bops. macOS installs it only when the zip's app is signed by the same team (A47GG9FV27) with the same bundle id (`ai.orgo.bops`), so `release.sh` checks the zip for that. A copy opened from the DMG or Downloads, or in a folder the user can't write, can't replace itself and sends the user to bops.bot instead. Versions 0.0.28 and older don't update themselves: their users download the next version from bops.bot once. To try an update on your Mac, run a build with `BOPS_UPDATE_FEED=http://127.0.0.1:/latest.json` (only this Mac's addresses are taken), from a local server holding that file and the zip. The app runs with the hardened runtime and three entitlements (`build/entitlements.mac.plist`): JIT for Electron, the microphone for calls, and Apple Events so Full access can use your apps. Bops asks macOS for Screen Recording, the microphone and notifications, and Full access also needs Full Disk Access and Automation. Bops isn't on the Mac App Store because its sandbox would stop Bops from running its own server, starting Codex and Chrome for the bots, and driving other apps. diff --git a/cloud/deploy/bops-cloud.service b/cloud/deploy/bops-cloud.service index c23dac98..0c54bb30 100644 --- a/cloud/deploy/bops-cloud.service +++ b/cloud/deploy/bops-cloud.service @@ -8,7 +8,8 @@ User=bops Group=bops WorkingDirectory=/opt/bops # Provider keys, the database URL and BOPS_CLOUD_SECRET (from Orgo's prod/bops-secrets.env), plus the -# public settings (prod/bops-public.env). scripts/cloud-deploy.sh writes it, mode 0640 root:bops. +# public settings (prod/bops-public.env). Orgo's cloud-deploy.sh (OrgoAI/bops-secrets) writes it, mode +# 0640 root:bops. EnvironmentFile=/etc/bops-cloud/env ExecStart=/usr/local/bin/node /opt/bops/cloud/server.ts Restart=always diff --git a/cloud/deploy/install.sh b/cloud/deploy/install.sh index 8b50b337..0088f67f 100755 --- a/cloud/deploy/install.sh +++ b/cloud/deploy/install.sh @@ -2,7 +2,8 @@ # Sets up Bops Cloud on orgo-web's production box (Ubuntu 24.04), as root, next to orgo-web. Safe to # run again. Run on the box: # bash /opt/bops/cloud/deploy/install.sh -# Then scripts/cloud-deploy.sh (on a laptop) puts the code, the settings and the relay binary here. +# Then Orgo's cloud-deploy.sh (OrgoAI/bops-secrets; on a laptop, from a bops checkout) puts the code, +# the settings and the relay binary here. # # What it adds, and nothing else: # - Node 24 (official build, checksum checked) under /usr/local/lib/nodejs. @@ -45,7 +46,7 @@ install -d -m 0750 -o root -g bops /etc/bops-cloud install -d -m 0750 -o root -g orgo-relay /etc/orgo-relay install -d -m 0750 -o orgo-relay -g orgo-relay /etc/orgo-relay/tls -# The services: installed and enabled; scripts/cloud-deploy.sh starts them once code and settings are in. +# The services: installed and enabled; Orgo's cloud-deploy.sh starts them once code and settings are in. install -m 0644 "$here/bops-cloud.service" "$here/orgo-relay-rendezvous.service" "$here/orgo-relay-cert.service" "$here/orgo-relay-cert.timer" /etc/systemd/system/ install -m 0755 "$here/orgo-relay-cert-sync.sh" /usr/local/sbin/orgo-relay-cert-sync systemctl daemon-reload @@ -71,4 +72,4 @@ else fi fi -echo "Box ready (Node $(/usr/local/bin/node --version)). Now run scripts/cloud-deploy.sh." +echo "Box ready (Node $(/usr/local/bin/node --version)). Now run Orgo's cloud-deploy.sh (OrgoAI/bops-secrets) from a bops checkout." diff --git a/desktop/main.cjs b/desktop/main.cjs index e994e8c4..29c91431 100644 --- a/desktop/main.cjs +++ b/desktop/main.cjs @@ -1208,7 +1208,7 @@ ipcMain.handle("mac-show-main", () => showMain()); /* * New versions. Bops gets them itself: it asks bops.bot for the newest release (download/latest.json, - * written by scripts/download-publish.sh) at launch and every hour, and when there's a newer one than + * written by Orgo's download-publish.sh) at launch and every hour, and when there's a newer one than * this app it's downloaded in the app (desktop/updates.cjs). With "Download updates automatically" on * (Settings → This Mac, the default) that happens quietly; with it off the page's notice * (components/app/update-notice.tsx) says it's out and the user clicks Download. Either way, once it's diff --git a/docs/releases/README.md b/docs/releases/README.md index 34723d26..df13462d 100644 --- a/docs/releases/README.md +++ b/docs/releases/README.md @@ -7,7 +7,7 @@ Between releases, the lines collect in `next.md`: add one when something users w Each version's lines become: - its GitHub release notes, with the line on signing and notarization under them; -- "What's new" in the app's notice that a newer Bops is out or ready to install, and on bops.bot's download card (both read `/download/latest.json`, which `scripts/download-publish.sh` writes). +- "What's new" in the app's notice that a newer Bops is out or ready to install, and on bops.bot's download card (both read `/download/latest.json`, which Orgo's `scripts/download-publish.sh`, also in OrgoAI/bops-secrets, writes). Write for the people using Bops: what they can do now, or what works better. Plain words: no branch names, pull requests, file paths or settings, and no names of people. diff --git a/scripts/cloud-deploy.sh b/scripts/cloud-deploy.sh deleted file mode 100755 index 5749ab42..00000000 --- a/scripts/cloud-deploy.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env bash -# Deploys Bops Cloud to its box (orgo-web's production box, where it runs next to orgo-web): the code -# (cloud/, db/), its settings, and the orgo-relay rendezvous; then restarts both and checks the cloud -# answers. The box is set up once with cloud/deploy/install.sh. Run from the repo: -# scripts/cloud-deploy.sh -# Needs: Orgo's private OrgoAI/bops-secrets next to this repo (BOPS_SECRETS points elsewhere) and sops, -# which decrypts its prod/bops-secrets.env; gh (fetches orgo-relay from its private releases); and SSH -# to the box as root (default key: z-legacy-fleet's id_ed25519; BOPS_CLOUD_SSH_KEY picks another). -set -euo pipefail -root="$(git rev-parse --show-toplevel)" -cd "$root" -# Orgo's prod settings and secrets sit next to the main checkout (this may be a worktree elsewhere). -main="$(cd "$(git rev-parse --git-common-dir)/.." && pwd)" -prod="${BOPS_SECRETS:-$main/../bops-secrets}/prod" -[ -f "$prod/bops-public.env" ] || { echo "No $prod/bops-public.env: clone OrgoAI/bops-secrets next to $main, or set BOPS_SECRETS." >&2; exit 1; } -set -a -. "$prod/bops-public.env" -set +a -[ -f "$prod/bops-secrets.env" ] || { echo "No $prod/bops-secrets.env." >&2; exit 1; } -: "${BOPS_CLOUD_BOX:?set BOPS_CLOUD_BOX (root@
) in bops-secrets prod/bops-public.env}" - -ssh_opts=(-o StrictHostKeyChecking=accept-new -o ConnectTimeout=15) -key="${BOPS_CLOUD_SSH_KEY:-$root/../z-legacy-fleet/.ssh/id_ed25519}" -[ -f "$key" ] && ssh_opts+=(-i "$key" -o IdentitiesOnly=yes) -box() { ssh "${ssh_opts[@]}" "$BOPS_CLOUD_BOX" "$@"; } - -if [ -n "$(git status --porcelain -- cloud db)" ]; then - echo "cloud/ or db/ has uncommitted changes: commit them first, so the box runs a known version." >&2 - exit 1 -fi -rev="$(git rev-parse --short HEAD)" - -# Only what the cloud uses: the public settings above, and of the secrets only the provider keys, -# Slack's signing secret, the database and the sealing key (not Stripe, Latitude or the Orgo admin token). -# Both are read before anything on the box changes, so a decrypt that fails stops here. -public_settings="$(grep -E '^(BOPS_CLOUD_PUBLIC_URL|BOPS_CLOUD_PORT|BOPS_ORGO_ORIGIN|OPENAI_SIP_URI|BOPS_VERIFY_EMAIL|BOPS_SLACK_APP_ID|BOPS_COMPOSIO_AUTH_CONFIGS|BOPS_AI_CREDITS|BOPS_PLAN_LIMITS|BOPS_MAIL_DOMAIN|BOPS_PHONE_AREA|BOPS_TELEMETRY)=' "$prod/bops-public.env")" -secret_settings="$(sops -d "$prod/bops-secrets.env" | grep -E '^(BOPS_DATABASE_URL|BOPS_CLOUD_SECRET|OPENAI_API_KEY|OPENAI_EXECUTOR_API_KEY|OPENAI_WEBHOOK_SECRET|DEEPINFRA_API_KEY|AGENTPHONE_API_KEY|AGENTMAIL_API_KEY|HONCHO_API_KEY|COMPOSIO_API_KEY|TYPESAFE_API_KEY|TREG_TOKEN|BOPS_SLACK_SIGNING_SECRET|BOPS_CLOUD_PLAN_SECRET|TWILIO_[A-Z_]+)=')" -grep -q '^BOPS_DATABASE_URL=' <<<"$secret_settings" || { echo "No BOPS_DATABASE_URL in $prod/bops-secrets.env: the box's settings stay as they are." >&2; exit 1; } - -echo "1/4 code ($rev)" -rsync -az --delete -e "ssh ${ssh_opts[*]}" --exclude node_modules --exclude test cloud db "$BOPS_CLOUD_BOX:/opt/bops/" -echo "$rev" | box 'cat > /opt/bops/REVISION' -box 'cd /opt/bops/cloud && npm install --omit=dev --no-audit --no-fund --no-package-lock --loglevel=error >/dev/null' - -echo "2/4 settings" -printf '%s\n%s\n' "$public_settings" "$secret_settings" | box 'install -m 0640 -o root -g bops /dev/stdin /etc/bops-cloud/env' -unset secret_settings - -echo "3/4 relay ($ORGO_RELAY_VERSION)" -scripts/relay-deploy.sh - -echo "4/4 restart and check" -box 'systemctl restart bops-cloud.service && sleep 3 && systemctl is-active bops-cloud.service' -curl -fsS --retry 5 --retry-delay 2 --retry-all-errors "$BOPS_CLOUD_PUBLIC_URL/health" -echo -echo "Bops Cloud $rev is live at $BOPS_CLOUD_PUBLIC_URL" diff --git a/scripts/download-publish.sh b/scripts/download-publish.sh deleted file mode 100755 index 435277ac..00000000 --- a/scripts/download-publish.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env bash -# Puts a release's Mac app on bops.bot: the DMG for the landing page's download buttons, -# https://bops.bot/download/Bops.dmg (always the newest release) and /download/Bops--arm64.dmg, -# and the zip the app updates itself from, /download/Bops--arm64-mac.zip (the newest three kept). -# Takes both from the GitHub release on OrgoAI/bops, checks them against the release's latest-mac.yml, -# copies them to the Bops VM (BOPS_SITE_BOX), then checks the VM serves the same bytes. Then it writes -# /download/latest.json, which the Mac app reads to learn a newer version is out and what's new in it, -# and downloads it from (its "currentRelease" and "releases" are what Electron's autoUpdater reads; see -# "New versions" in desktop/main.cjs), and bops.bot's download card reads for its "What's new". Apps -# before 0.0.29 read only "version" and "notes", and send their users to bops.bot. Run from the repo -# after a release: -# scripts/download-publish.sh # the newest release -# scripts/download-publish.sh v0.0.2 # a given one -# Needs Orgo's private OrgoAI/bops-secrets next to this repo (its prod/bops-public.env names the VM; -# BOPS_SECRETS points elsewhere), gh, jq, and SSH to the VM as root (default key: z-legacy-fleet's -# ci-staging.key; BOPS_SITE_SSH_KEY picks another). -set -euo pipefail -root="$(git rev-parse --show-toplevel)" -cd "$root" -command -v jq >/dev/null || { echo "latest.json is made with jq: install it first (brew install jq)." >&2; exit 1; } -# Orgo's prod settings and the legacy key sit next to the main checkout (this may be a worktree elsewhere). -main="$(cd "$(git rev-parse --git-common-dir)/.." && pwd)" -prod="${BOPS_SECRETS:-$main/../bops-secrets}/prod" -[ -f "$prod/bops-public.env" ] || { echo "No $prod/bops-public.env: clone OrgoAI/bops-secrets next to $main, or set BOPS_SECRETS." >&2; exit 1; } -set -a -. "$prod/bops-public.env" -set +a -: "${BOPS_SITE_BOX:?set BOPS_SITE_BOX (root@
) in bops-secrets prod/bops-public.env}" - -ssh_opts=(-o StrictHostKeyChecking=accept-new -o ConnectTimeout=15) -key="${BOPS_SITE_SSH_KEY:-$main/../z-legacy-fleet/.ssh/ci-staging.key}" -[ -f "$key" ] && ssh_opts+=(-i "$key" -o IdentitiesOnly=yes) -box() { ssh "${ssh_opts[@]}" "$BOPS_SITE_BOX" "$@"; } - -tag="${1:-$(gh release list -R OrgoAI/bops --exclude-drafts --exclude-pre-releases --limit 1 --json tagName -q '.[0].tagName')}" -dmg="Bops-${tag#v}-arm64.dmg" -zip="Bops-${tag#v}-arm64-mac.zip" -tmp="$(mktemp -d)" -trap 'rm -rf "$tmp"' EXIT - -echo "1/4 $dmg and $zip from the $tag release" -gh release download "$tag" -R OrgoAI/bops -p "$dmg" -p "$zip" -p latest-mac.yml -D "$tmp" >/dev/null -# latest-mac.yml (written by electron-builder) lists each file's sha512, base64. -sha() { openssl dgst -sha512 -binary "$1" | base64 | tr -d '\n'; } -listed() { awk -v f="$1" '$0 ~ "url: "f"$" {getline; sub(/^ *sha512: */, ""); print; exit}' "$tmp/latest-mac.yml"; } -want="$(listed "$dmg")" -zip_want="$(listed "$zip")" -if [ -z "$want" ] || [ "$(sha "$tmp/$dmg")" != "$want" ]; then - echo "$dmg doesn't match the release's latest-mac.yml: not publishing it." >&2 - exit 1 -fi -if [ -z "$zip_want" ] || [ "$(sha "$tmp/$zip")" != "$zip_want" ]; then - echo "$zip doesn't match the release's latest-mac.yml: not publishing it." >&2 - exit 1 -fi -# The apps install the zip's app without checking it's notarized (only its signature): checked here. -ditto -x -k "$tmp/$zip" "$tmp/unz" -if ! xcrun stapler validate "$tmp/unz/Bops.app" >/dev/null; then - echo "The app in $zip isn't notarized and stapled: not publishing it." >&2 - exit 1 -fi -rm -rf "$tmp/unz" - -# latest.json, made now, so nothing changes on the VM when it can't be: the version, where to get it, when -# it came out, and "notes", what's new in it. Those are the "- " lines of docs/releases/.md as tagged -# on OrgoAI/bops, else of this checkout's (a tag from before those files); with neither, the file has no -# "notes", as before. Not the release's own notes: older ones have long lines and details that aren't for -# the app or the page. jq writes it, so quotes and any script in the notes come through intact (as \u -# escapes: the file is plain ASCII). -released="$(gh release view "$tag" -R OrgoAI/bops --json publishedAt -q .publishedAt 2>/dev/null || true)" -if ! md="$(gh api -H "Accept: application/vnd.github.raw" "repos/OrgoAI/bops/contents/docs/releases/$tag.md?ref=$tag" 2>/dev/null)"; then - md="$(cat "docs/releases/$tag.md" 2>/dev/null || true)" -fi -printf '%s\n' "$md" | tr -d '\r' | sed -nE 's/^-[[:space:]]+//p' > "$tmp/notes.txt" -# currentRelease and releases are Squirrel.Mac's (Electron's autoUpdater, "json" feed): the version to -# update to and its zip. It fetches whatever currentRelease names, so this file only ever names the newest. -jq -nac --arg version "${tag#v}" --arg released "${released:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}" --arg zip "https://bops.bot/download/$zip" --rawfile notes "$tmp/notes.txt" ' - [$notes | splits("\n") | sub("\\s+$"; "") | select(length > 0)] as $lines - | {version: $version, url: "https://bops.bot", dmg: "https://bops.bot/download/Bops.dmg", released: $released} - + (if ($lines | length) > 0 then {notes: $lines} else {} end) - + {currentRelease: $version, - releases: [{version: $version, updateTo: {version: $version, name: $version, notes: ($lines | join("\n")), pub_date: $released, url: $zip}}]}' > "$tmp/latest.json" - -echo "2/4 to the VM" -box 'install -d -m 0755 /opt/bops/download' -rsync -a --partial -e "ssh ${ssh_opts[*]}" "$tmp/$dmg" "$BOPS_SITE_BOX:/opt/bops/download/$dmg.part" -# Renamed into place, then Bops.dmg is pointed at it in one step, so nobody gets half a file. -box "cd /opt/bops/download && chmod 0644 '$dmg.part' && mv -f '$dmg.part' '$dmg' && ln -f '$dmg' Bops.dmg.new && mv -f Bops.dmg.new Bops.dmg" -# The zip the apps update from, the same way. About 155 MB each: the newest three are kept (an app -# mid-download of the one before keeps its file). -rsync -a --partial -e "ssh ${ssh_opts[*]}" "$tmp/$zip" "$BOPS_SITE_BOX:/opt/bops/download/$zip.part" -box "cd /opt/bops/download && chmod 0644 '$zip.part' && mv -f '$zip.part' '$zip' && ls -1t -- *-arm64-mac.zip | tail -n +4 | xargs -r rm -f --" - -echo "3/4 check" -remote="$(box "openssl dgst -sha512 -binary /opt/bops/download/Bops.dmg | base64 | tr -d '\n'")" -ip="${BOPS_SITE_BOX#*@}" -size="$(curl -fsSI --max-time 15 --resolve "bops.bot:443:$ip" https://bops.bot/download/Bops.dmg | awk 'tolower($1)=="content-length:" {print $2+0}')" -if [ "$remote" != "$want" ] || [ "$size" != "$(wc -c < "$tmp/$dmg" | tr -d ' ')" ]; then - echo "bops.bot doesn't serve $dmg correctly yet (checksum or size differs)." >&2 - exit 1 -fi -echo "https://bops.bot/download/Bops.dmg is $dmg ($((size / 1048576)) MB)" -# The zip as an app gets it: every byte, through Caddy. -curl -fsS --max-time 600 --resolve "bops.bot:443:$ip" -o "$tmp/served.zip" "https://bops.bot/download/$zip" -if [ "$(sha "$tmp/served.zip")" != "$zip_want" ] || [ "$(wc -c < "$tmp/served.zip")" != "$(wc -c < "$tmp/$zip")" ]; then - echo "bops.bot doesn't serve $zip correctly yet (checksum or size differs)." >&2 - exit 1 -fi -rm -f "$tmp/served.zip" -echo "https://bops.bot/download/$zip is served" - -# Only once the DMG and the zip are served: the apps that read this download the zip, or send their -# users to the DMG. -echo "4/4 latest.json" -# Written next to it, then renamed into place, so an app never reads half a file. -box "cat > /opt/bops/download/latest.json.new && chmod 0644 /opt/bops/download/latest.json.new && mv -f /opt/bops/download/latest.json.new /opt/bops/download/latest.json" < "$tmp/latest.json" -served="$(curl -fsS --max-time 15 --resolve "bops.bot:443:$ip" https://bops.bot/download/latest.json || true)" -if [ "$served" != "$(cat "$tmp/latest.json")" ]; then - echo "bops.bot doesn't serve the new latest.json yet: got ${served:-nothing}" >&2 - exit 1 -fi -echo "https://bops.bot/download/latest.json says ${tag#v} (what's new: $(jq '.notes // [] | length' "$tmp/latest.json") lines)" diff --git a/scripts/relay-deploy.sh b/scripts/relay-deploy.sh deleted file mode 100755 index 9a807b49..00000000 --- a/scripts/relay-deploy.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash -# Deploys the orgo-relay rendezvous to orgo-web's production box, where it runs next to Bops Cloud. -# It's what Orgo's "Use my device's IP" (and so Bops' "Route through this Mac") connects through: -# Macs dial it and hold a connection, and bot computers' browsers go out through them. This puts the -# release named by ORGO_RELAY_VERSION on the box with its secret and Caddy's certificate for -# RELAY_HOST, restarts it, and checks it answers at https://RELAY_HOST:8443. The box is set up once -# with cloud/deploy/install.sh; scripts/cloud-deploy.sh runs this too. Run from the repo: -# scripts/relay-deploy.sh -# Needs: Orgo's private OrgoAI/bops-secrets next to this repo (BOPS_SECRETS points elsewhere) and sops, -# which decrypts its prod/bops-secrets.env; gh (orgo-relay's releases are private); and SSH to the box -# as root (default key: z-legacy-fleet's id_ed25519; BOPS_CLOUD_SSH_KEY picks another). -# -# ORGO_RELAY_SECRET has to be the same value as in orgo-web's envs/prod/web-secrets.env: orgo-web -# makes every device's and computer's credential from it, and the rendezvous checks them with it. -set -euo pipefail -root="$(git rev-parse --show-toplevel)" -cd "$root" -# Orgo's prod settings and secrets sit next to the main checkout (this may be a worktree elsewhere). -main="$(cd "$(git rev-parse --git-common-dir)/.." && pwd)" -prod="${BOPS_SECRETS:-$main/../bops-secrets}/prod" -[ -f "$prod/bops-public.env" ] || { echo "No $prod/bops-public.env: clone OrgoAI/bops-secrets next to $main, or set BOPS_SECRETS." >&2; exit 1; } -set -a -. "$prod/bops-public.env" -set +a -: "${BOPS_CLOUD_BOX:?set BOPS_CLOUD_BOX (root@
) in bops-secrets prod/bops-public.env}" -: "${ORGO_RELAY_VERSION:?set ORGO_RELAY_VERSION (an orgo-relay release, like v0.0.3) in bops-secrets prod/bops-public.env}" -: "${RELAY_HOST:?set RELAY_HOST (the name Caddy has a certificate for) in bops-secrets prod/bops-public.env}" - -ssh_opts=(-o StrictHostKeyChecking=accept-new -o ConnectTimeout=15) -key="${BOPS_CLOUD_SSH_KEY:-$root/../z-legacy-fleet/.ssh/id_ed25519}" -[ -f "$key" ] && ssh_opts+=(-i "$key" -o IdentitiesOnly=yes) -box() { ssh "${ssh_opts[@]}" "$BOPS_CLOUD_BOX" "$@"; } - -secret="$(sops -d "$prod/bops-secrets.env" | sed -n 's/^ORGO_RELAY_SECRET=//p')" -if [ -z "$secret" ]; then - echo " no ORGO_RELAY_SECRET in bops-secrets.env yet: rendezvous left as it is" - exit 0 -fi - -tmp="$(mktemp -d)" -trap 'rm -rf "$tmp"' EXIT -gh release download "$ORGO_RELAY_VERSION" -R orgoai/orgo-relay -p orgo-relay-linux-amd64 -p SHA256SUMS -D "$tmp" >/dev/null -(cd "$tmp" && grep ' orgo-relay-linux-amd64$' SHA256SUMS | shasum -a 256 -c - >/dev/null) -rsync -az -e "ssh ${ssh_opts[*]}" "$tmp/orgo-relay-linux-amd64" "$BOPS_CLOUD_BOX:/usr/local/bin/orgo-relay.new" -box 'chmod 0755 /usr/local/bin/orgo-relay.new && mv /usr/local/bin/orgo-relay.new /usr/local/bin/orgo-relay' -printf 'ORGO_RELAY_SECRET=%s\nRELAY_HOST=%s\n' "$secret" "$RELAY_HOST" | box 'install -m 0640 -o root -g orgo-relay /dev/stdin /etc/orgo-relay/env' -# The certificate first (the rendezvous won't start without it), then the timer that picks up -# Caddy's renewals, then the rendezvous itself. -box 'systemctl start orgo-relay-cert.service && systemctl enable --now orgo-relay-cert.timer >/dev/null 2>&1 && test -s /etc/orgo-relay/tls/cert.pem && systemctl restart orgo-relay-rendezvous.service && sleep 2 && systemctl is-active orgo-relay-rendezvous.service' - -health="$(curl -fsS --retry 5 --retry-delay 2 --retry-all-errors "https://$RELAY_HOST:8443/healthz")" -case "$health" in - *"\"version\":\"$ORGO_RELAY_VERSION\""*) echo " rendezvous $ORGO_RELAY_VERSION answers at https://$RELAY_HOST:8443 ($health)" ;; - *) echo "The rendezvous answered, but not as $ORGO_RELAY_VERSION: $health" >&2; exit 1 ;; -esac diff --git a/scripts/site-deploy.sh b/scripts/site-deploy.sh deleted file mode 100755 index 2938d272..00000000 --- a/scripts/site-deploy.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -# Deploys the landing page (site/) to bops.bot: the Bops VM (BOPS_SITE_BOX) serves it with Caddy. -# Copies the page's files and site/deploy/Caddyfile, reloads Caddy, and checks that bops.bot on the VM -# serves this index.html. The VM is set up once with site/deploy/install.sh. The backend API -# (bops.orgo.ai/api) is separate: scripts/cloud-deploy.sh. Run from the repo: -# scripts/site-deploy.sh -# Needs Orgo's private OrgoAI/bops-secrets next to this repo (its prod/bops-public.env names the VM; -# BOPS_SECRETS points elsewhere) and SSH to the VM as root (default key: z-legacy-fleet's -# ci-staging.key; BOPS_SITE_SSH_KEY picks another). -set -euo pipefail -root="$(git rev-parse --show-toplevel)" -cd "$root" -# Orgo's prod settings and the legacy key sit next to the main checkout (this may be a worktree elsewhere). -main="$(cd "$(git rev-parse --git-common-dir)/.." && pwd)" -prod="${BOPS_SECRETS:-$main/../bops-secrets}/prod" -[ -f "$prod/bops-public.env" ] || { echo "No $prod/bops-public.env: clone OrgoAI/bops-secrets next to $main, or set BOPS_SECRETS." >&2; exit 1; } -set -a -. "$prod/bops-public.env" -set +a -: "${BOPS_SITE_BOX:?set BOPS_SITE_BOX (root@
) in bops-secrets prod/bops-public.env}" - -ssh_opts=(-o StrictHostKeyChecking=accept-new -o ConnectTimeout=15) -key="${BOPS_SITE_SSH_KEY:-$main/../z-legacy-fleet/.ssh/ci-staging.key}" -[ -f "$key" ] && ssh_opts+=(-i "$key" -o IdentitiesOnly=yes) -box() { ssh "${ssh_opts[@]}" "$BOPS_SITE_BOX" "$@"; } - -dir=site url=https://bops.bot/ - -if [ -n "$(git status --porcelain -- site)" ]; then - echo "site/ has uncommitted changes: commit them first, so bops.bot shows a known version." >&2 - exit 1 -fi -rev="$(git rev-parse --short HEAD)" - -echo "1/3 files ($rev)" -# Only what the page serves: not the deploy files, the old Fly setup or the README. -# site/90s stays off the box until its copy matches the app and its sign-up form keeps the emails. -rsync -az --delete -e "ssh ${ssh_opts[*]}" --exclude deploy --exclude Dockerfile --exclude fly.toml --exclude 90s \ - --exclude nginx.conf --exclude README.md site/ "$BOPS_SITE_BOX:/opt/bops/$dir/" -echo "$rev" | box "cat > /opt/bops/$dir-revision" - -echo "2/3 Caddy" -rsync -az -e "ssh ${ssh_opts[*]}" site/deploy/Caddyfile "$BOPS_SITE_BOX:/etc/caddy/Caddyfile.new" -box 'caddy validate --config /etc/caddy/Caddyfile.new --adapter caddyfile >/dev/null 2>&1 \ - && mv /etc/caddy/Caddyfile.new /etc/caddy/Caddyfile && systemctl reload caddy' - -echo "3/3 check" -# Asked of the VM itself (bops.bot may still resolve to the old host for a while after a DNS change), -# and compared with this index.html, so a stale copy elsewhere can't pass for it. -ip="${BOPS_SITE_BOX#*@}" -want="$(shasum -a 256 site/index.html | cut -d' ' -f1)" -for _ in $(seq 1 30); do - got="$(curl -fsS --max-time 10 --resolve "bops.bot:443:$ip" "$url" 2>/dev/null | shasum -a 256 | cut -d' ' -f1)" || true - [ "$got" = "$want" ] && break - sleep 4 -done -if [ "$got" != "$want" ]; then - echo "$url on $ip doesn't serve this index.html yet (a new certificate can take a minute; see journalctl -u caddy)." >&2 - exit 1 -fi -echo "$url shows $rev (from $ip)" diff --git a/scripts/test-site.mjs b/scripts/test-site.mjs index 4155d540..61377afd 100644 --- a/scripts/test-site.mjs +++ b/scripts/test-site.mjs @@ -278,7 +278,7 @@ await test("an Intel Mac gets the Apple silicon note", async () => { await p.context().close(); }); -// latest.json as scripts/download-publish.sh writes it (jq, ASCII with \u escapes), with notes that have +// latest.json as Orgo's download-publish.sh writes it (jq, ASCII with \u escapes), with notes that have // quotes, an apostrophe, other scripts, an emoji and markup, and one line more than the card shows. const NOTES = ['Say "hi" to Boppy\'s new voice', "Números en español, 日本語 and 🎧", 'Not bold & ', "Four", "Five", "Six"]; const LATEST = JSON.stringify({ version: "0.0.11", url: "https://bops.bot", dmg: "https://bops.bot/download/Bops.dmg", released: "2026-10-06T17:00:00Z", notes: NOTES }).replace( diff --git a/site/README.md b/site/README.md index 4729ccc0..37c0ef1f 100644 --- a/site/README.md +++ b/site/README.md @@ -11,7 +11,7 @@ python3 -m http.server 4321 --directory site # then open http://localhost:4321 - `main.js`: the illustrated scenes (hero cards, badge, thread) keep their designed size and scale down to fit smaller screens. Below 1180 px the fit is content-aware (it crops a scene's empty margins, so the scene draws larger), and on phones `styles.css` recomposes the hero and the thread into a narrower layout of the same parts. From 1180 px up the page renders exactly as before. - `lanyard.js`: the badge in "Hire a bot" drops onto its lanyard (with a little wind) the first time it scrolls into view, then swings when you hover, drag or tap it (a few springs and a CSS 3D transform). At rest, with reduced motion or without JavaScript it is exactly the static design. - `notes.js`: music notes drift out of the hero Boppy's headphones; clicking or tapping it makes a burst of them. Off with reduced motion. -- `download.js`: the three Download for Mac buttons go to `/download/Bops.dmg`. On a Mac they open Get First Access first: leave an email (it goes to PostHog) or skip, and either way the download starts and the card says so. At the card's foot, What's new in the newest release: its version and lines from `/download/latest.json` (written with each release by `scripts/download-publish.sh`), left out when that can't be read. Anywhere else (iPhone, iPad, Android, Windows, Linux) they open a short note to get Bops on a Mac instead, with Send to my Mac (the share sheet, so AirDrop or Messages) and Copy link. An Intel Mac in Chrome gets the note's Apple silicon version. Without JS the buttons simply download. Test it with `node scripts/test-site.mjs`. +- `download.js`: the three Download for Mac buttons go to `/download/Bops.dmg`. On a Mac they open Get First Access first: leave an email (it goes to PostHog) or skip, and either way the download starts and the card says so. At the card's foot, What's new in the newest release: its version and lines from `/download/latest.json` (written with each release by Orgo's `scripts/download-publish.sh`, in its private OrgoAI/bops-secrets), left out when that can't be read. Anywhere else (iPhone, iPad, Android, Windows, Linux) they open a short note to get Bops on a Mac instead, with Send to my Mac (the share sheet, so AirDrop or Messages) and Copy link. An Intel Mac in Chrome gets the note's Apple silicon version. Without JS the buttons simply download. Test it with `node scripts/test-site.mjs`. - Pricing (`#pricing`, linked from the nav): Free, Pro and Max in exactly the app's words (`PLAN_CARDS` in `lib/plan-includes.ts`, which the app's Settings shows); `scripts/test-site.mjs` fails when the two differ. Change the plans there and here together. - PostHog: the snippet in `index.html` sends to Orgo's production project, with `site: bops.bot` on every event. The card's events are `bops_download_click` (with which button: nav, hero or footer), `bops_download_email` (and an identify by the email), `bops_download_skip_email` and `bops_download_note` (the non-Mac or Intel note). Every call is guarded, so a blocker can't break the buttons. - `styles.css` and the scripts are cached for a day on bops.bot and the HTML isn't: when one changes, bump its `?v=` in `index.html`. diff --git a/site/deploy/Caddyfile b/site/deploy/Caddyfile index 1cd27b51..53b4875d 100644 --- a/site/deploy/Caddyfile +++ b/site/deploy/Caddyfile @@ -1,7 +1,7 @@ -# bops.bot: the landing page (site/), as plain files, on the Bops VM. scripts/site-deploy.sh installs -# this as /etc/caddy/Caddyfile; Caddy gets and renews the certificates itself. HTML is always fresh, -# everything else is cached a day (as nginx.conf did on Fly). The backend API is not here: it's -# bops.orgo.ai/api (cloud/, scripts/cloud-deploy.sh). +# bops.bot: the landing page (site/), as plain files, on the Bops VM. Orgo's site-deploy.sh +# (OrgoAI/bops-secrets) installs this as /etc/caddy/Caddyfile; Caddy gets and renews the certificates +# itself. HTML is always fresh, everything else is cached a day (as nginx.conf did on Fly). The backend +# API is not here: it's bops.orgo.ai/api (cloud/, Orgo's cloud-deploy.sh). bops.bot { root * /opt/bops/site encode zstd gzip @@ -21,7 +21,7 @@ bops.bot { respond "ok" 200 } # The Mac app for the page's download buttons: /download/Bops.dmg is always the newest release, - # /download/Bops--arm64.dmg a given one (scripts/download-publish.sh puts them there). + # /download/Bops--arm64.dmg a given one (Orgo's download-publish.sh puts them there). # /download/Bops--arm64-mac.zip is what the installed app updates itself from, and # /download/latest.json what it reads to learn a newer version is out: no-cache like the pages # (@pages above), so a release shows at once; neither fetch() nor the updater minds the attachment header. diff --git a/site/deploy/install.sh b/site/deploy/install.sh index 9013bf8a..589d26d9 100755 --- a/site/deploy/install.sh +++ b/site/deploy/install.sh @@ -3,7 +3,8 @@ # it gets and renews the certificates) and a firewall that lets in SSH, HTTP and HTTPS only. The VM is # "bops-prod-ash" (vm.small, Ashburn, Ubuntu 24.04) in Latitude's "bops" project. Run as root: # scp -r site/deploy root@:/tmp/ && ssh root@ bash /tmp/deploy/install.sh -# Then scripts/site-deploy.sh puts the page and the Caddyfile in place. +# Then Orgo's site-deploy.sh (OrgoAI/bops-secrets; from a bops checkout) puts the page and the Caddyfile +# in place. set -euo pipefail export DEBIAN_FRONTEND=noninteractive @@ -26,4 +27,4 @@ ufw default allow outgoing >/dev/null for p in 22/tcp 80/tcp 443/tcp 443/udp; do ufw allow "$p" >/dev/null; done ufw --force enable >/dev/null -echo "VM ready ($(caddy version | cut -d' ' -f1), firewall on). Now run scripts/site-deploy.sh." +echo "VM ready ($(caddy version | cut -d' ' -f1), firewall on). Now run Orgo's site-deploy.sh (OrgoAI/bops-secrets) from a bops checkout." diff --git a/site/download.js b/site/download.js index 605a8027..408c769a 100644 --- a/site/download.js +++ b/site/download.js @@ -42,7 +42,7 @@ let access, ask, form, email, error, done, doneTitle, from; // What's new in the newest release: its version and a few short lines from /download/latest.json, which - // scripts/download-publish.sh writes with each release. Read once on a Mac as the page loads, and shown + // Orgo's download-publish.sh writes with each release. Read once on a Mac as the page loads, and shown // as text at the foot of Get First Access. When it can't be read or has no lines, the card is as it was. let news; function whatsNew(latest) {