diff --git a/.gitea/workflows/build-dev.yaml b/.gitea/workflows/build-dev.yaml index dabae5c..7a8a765 100644 --- a/.gitea/workflows/build-dev.yaml +++ b/.gitea/workflows/build-dev.yaml @@ -7,8 +7,8 @@ on: workflow_dispatch: env: - GO_VERSION: "1.23" - NODE_VERSION: "20" + GO_VERSION: "1.25" + NODE_VERSION: "22" BINARY_NAME: remoterig jobs: @@ -39,25 +39,34 @@ jobs: go build -ldflags="-s -w -X main.version=${GITHUB_SHA:0:8}" \ -o ${{ env.BINARY_NAME }} ./cmd/server - - name: Upload build artifact - uses: actions/upload-artifact@v4 - with: - name: ${{ env.BINARY_NAME }} - path: ${{ env.BINARY_NAME }} - retention-days: 5 + # Pull-based deploy: publish the binary to a rolling "dev" release. + # The Pi polls this release and self-updates (scripts/pi-update.sh); + # the runner never needs to reach the closed RemoteRig network. + - name: Publish to rolling dev release + env: + TOKEN: ${{ secrets.GITHUB_TOKEN }} + SERVER: ${{ github.server_url }} + REPO: ${{ github.repository }} + SHA: ${{ github.sha }} + run: | + set -euo pipefail + command -v jq >/dev/null || sudo apt-get update -qq && sudo apt-get install -y -qq jq + API="$SERVER/api/v1/repos/$REPO" + AUTH="Authorization: token $TOKEN" + VERSION="${SHA:0:8}" + echo "$VERSION" > version.txt + sha256sum "$BINARY_NAME" | awk '{print $1}' > "$BINARY_NAME.sha256" - - name: Trigger deploy workflow - if: success() - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - await github.rest.repos.createDispatchEvent({ - owner: context.repo.owner, - repo: context.repo.repo, - event_type: 'dev-build-success', - client_payload: { - sha: context.sha, - ref: context.ref - } - }) \ No newline at end of file + # Roll the "dev" release forward to this commit (delete old release + tag). + REL_ID=$(curl -sf -H "$AUTH" "$API/releases/tags/dev" | jq -r '.id // empty' || true) + [ -n "$REL_ID" ] && curl -sf -X DELETE -H "$AUTH" "$API/releases/$REL_ID" || true + curl -sf -X DELETE -H "$AUTH" "$API/tags/dev" || true + + REL_ID=$(curl -sf -X POST -H "$AUTH" -H "Content-Type: application/json" "$API/releases" \ + -d "{\"tag_name\":\"dev\",\"target_commitish\":\"$SHA\",\"name\":\"dev ($VERSION)\",\"body\":\"Rolling dev build $SHA\",\"prerelease\":true}" \ + | jq -r '.id') + + for f in "$BINARY_NAME" "$BINARY_NAME.sha256" version.txt; do + curl -sf -X POST -H "$AUTH" -F "attachment=@$f" "$API/releases/$REL_ID/assets?name=$f" + done + echo "Published dev release $VERSION" \ No newline at end of file diff --git a/.gitea/workflows/deploy-dev.yaml b/.gitea/workflows/deploy-dev.yaml deleted file mode 100644 index 4192f3a..0000000 --- a/.gitea/workflows/deploy-dev.yaml +++ /dev/null @@ -1,115 +0,0 @@ -name: Deploy (Dev) - -on: - repository_dispatch: - types: - - dev-build-success - workflow_dispatch: - -env: - BINARY_NAME: remoterig - DEV_HOST: ${{ secrets.DEV_HOST }} - DEV_USER: ${{ secrets.DEV_USER }} - DEPLOY_PATH: /opt/remoterig/remoterig - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Download build artifact - uses: actions/download-artifact@v4 - with: - name: ${{ env.BINARY_NAME }} - - - name: Ensure binary is executable - run: chmod +x ${{ env.BINARY_NAME }} - - - name: Write deploy script - run: | - cat > deploy.sh <<'SCRIPT' - #!/usr/bin/env bash - set -euo pipefail - BINARY="${1:-remoterig}" - DEPLOY_PATH="${2:-/opt/remoterig/remoterig}" - SERVICE="${3:-remoterig}" - TIMESTAMP=$(date +%Y%m%d%H%M%S) - BACKUP="${DEPLOY_PATH}.${TIMESTAMP}.bak" - - echo "::backup:: copying current binary" - if [ -f "$DEPLOY_PATH" ]; then - cp "$DEPLOY_PATH" "$BACKUP" - fi - - echo "::deploy:: installing new binary" - cp "$BINARY" "$DEPLOY_PATH" - chmod +x "$DEPLOY_PATH" - - echo "::restart:: reloading service" - systemctl reload-or-restart "$SERVICE" || systemctl restart "$SERVICE" - - echo "::health:: waiting for service" - sleep 3 - if systemctl is-active --quiet "$SERVICE"; then - echo "deploy ok — ${SERVICE} is active" - else - echo "::rollback:: service failed, restoring backup" - if [ -f "$BACKUP" ]; then - cp "$BACKUP" "$DEPLOY_PATH" - systemctl restart "$SERVICE" - fi - echo "rolled back to previous binary" - exit 1 - fi - - echo "::cleanup:: removing old backups (keeping last 3)" - ls -t "${DEPLOY_PATH}."*.bak 2>/dev/null | tail -n +4 | xargs -r rm -f - SCRIPT - chmod +x deploy.sh - - - name: Deploy config.yaml (if present) - run: | - if [ -f config.yaml ]; then - echo "config.yaml found, will deploy alongside binary" - echo "config.yaml" >> deploy-files.txt - else - echo "no config.yaml in repo, skipping" - fi - - - name: Deploy to dev server - uses: appleboy/scp-action@v0.1.7 - with: - host: ${{ env.DEV_HOST }} - username: ${{ env.DEV_USER }} - key: ${{ secrets.DEV_SSH_KEY }} - source: "${{ env.BINARY_NAME }},deploy.sh,config.yaml" - target: "/tmp/remoterig-deploy" - - - name: Execute deploy on dev server - uses: appleboy/ssh-action@v1 - with: - host: ${{ env.DEV_HOST }} - username: ${{ env.DEV_USER }} - key: ${{ secrets.DEV_SSH_KEY }} - script: | - set -euo pipefail - cd /tmp/remoterig-deploy - sudo ./deploy.sh "${{ env.BINARY_NAME }}" "${{ env.DEPLOY_PATH }}" "remoterig" - if [ -f config.yaml ]; then - echo "::config:: deploying config.yaml" - sudo mkdir -p "$(dirname "${{ env.DEPLOY_PATH }}")" - sudo cp config.yaml "$(dirname "${{ env.DEPLOY_PATH }}")/config.yaml" - fi - rm -rf /tmp/remoterig-deploy - - - name: Notify on failure - if: failure() - uses: appleboy/ssh-action@v1 - with: - host: ${{ env.DEV_HOST }} - username: ${{ env.DEV_USER }} - key: ${{ secrets.DEV_SSH_KEY }} - script: | - echo "deploy failed for commit ${{ github.sha }} on ${{ github.repository }}" > /tmp/remoterig-deploy-failure.txt diff --git a/README.md b/README.md index 93d849b..9738160 100644 --- a/README.md +++ b/README.md @@ -117,31 +117,39 @@ Platform: pi-zero-2w (max 16 cameras) RemoteRig hub ready ``` -## Building for Raspberry Pi Zero 2 W +## Deployment (CI/CD — pull-based) -Cross-compile from your development machine: +Deploys are automated and pull-based, so nothing has to reach into the closed +RemoteRig network: + +1. **Push to `dev`** → Gitea Actions (`.gitea/workflows/build-dev.yaml`) builds the + React frontend and cross-compiles the Go hub for **arm64**. +2. The workflow publishes the binary + `sha256` + `version.txt` to a rolling + **`dev` release**. +3. On the Pi, `remoterig-update.timer` runs `scripts/pi-update.sh` every few + minutes: it compares versions, downloads + verifies the checksum, and installs + via `scripts/deploy.sh` (backup → restart → rollback on failure). + +First-time Pi setup (`sudo scripts/setup-pi.sh`) installs Mosquitto, the +`remoterig` service, and the updater timer. If the repo is private, set a read +token in `/opt/remoterig/update.env`. + +### Manual / local cross-compile + +The Pi Zero 2 W is a Cortex-A53 (ARMv8) running 64-bit Raspberry Pi OS, so the +target is **arm64**: ```bash -GOOS=linux GOARCH=arm GOARM=6 go build -o remoterig-hub ./cmd/server/ -``` - -Copy the binary and `config.yaml` to your Pi: - -```bash -scp remoterig-hub config.yaml pi@raspberrypi:/home/pi/remoterig/ -``` - -Then run on the Pi: - -```bash -./remoterig-hub +GOOS=linux GOARCH=arm64 go build -o remoterig-hub ./cmd/server/ +scp remoterig-hub config.yaml pi@192.168.8.56:/opt/remoterig/ ``` ### Build Matrix | Target | Command | | ------ | ------- | -| Raspberry Pi Zero 2 W | `GOOS=linux GOARCH=arm GOARM=6 go build -o remoterig-hub ./cmd/server/` | +| Raspberry Pi Zero 2 W (64-bit OS) | `GOOS=linux GOARCH=arm64 go build -o remoterig-hub ./cmd/server/` | +| Raspberry Pi (32-bit OS) | `GOOS=linux GOARCH=arm GOARM=7 go build -o remoterig-hub ./cmd/server/` | | Local (same arch) | `go build -o remoterig-hub ./cmd/server/` | | Linux amd64 | `GOOS=linux GOARCH=amd64 go build -o remoterig-hub ./cmd/server/` | diff --git a/scripts/pi-update.sh b/scripts/pi-update.sh new file mode 100755 index 0000000..aed24e9 --- /dev/null +++ b/scripts/pi-update.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# RemoteRig — Pi-side pull updater +# ================================ +# Polls the rolling "dev" release on Gitea and, when the published version +# differs from what's installed, downloads + verifies (sha256) + deploys it +# via the existing rollback-capable deploy.sh. Run on a timer (see +# remoterig-update.timer). The Pi pulls; nothing pushes into the closed net. +# +# Config (env, or /opt/remoterig/update.env): +# GITEA_BASE default https://code.cubecraftcreations.com +# REPO default CubeCraft-Creations/remote-rig +# GITEA_TOKEN read token (required only if the repo is private) +# DEPLOY_PATH default /opt/remoterig/remoterig +# SERVICE default remoterig + +set -euo pipefail + +ENV_FILE="${ENV_FILE:-/opt/remoterig/update.env}" +# shellcheck disable=SC1090 +[ -f "$ENV_FILE" ] && . "$ENV_FILE" + +GITEA_BASE="${GITEA_BASE:-https://code.cubecraftcreations.com}" +REPO="${REPO:-CubeCraft-Creations/remote-rig}" +DEPLOY_DIR="/opt/remoterig" +DEPLOY_PATH="${DEPLOY_PATH:-$DEPLOY_DIR/remoterig}" +SERVICE="${SERVICE:-remoterig}" +TAG="dev" +DL="$GITEA_BASE/$REPO/releases/download/$TAG" +VERSION_FILE="$DEPLOY_DIR/VERSION" + +AUTH=() +[ -n "${GITEA_TOKEN:-}" ] && AUTH=(-H "Authorization: token $GITEA_TOKEN") + +log() { echo "[$(date -Is)] $*"; } + +# 1. What version is published? +REMOTE_VER="$(curl -fsSL "${AUTH[@]}" "$DL/version.txt" | tr -d '[:space:]')" || { + log "could not reach $DL/version.txt — skipping"; exit 0; } +[ -n "$REMOTE_VER" ] || { log "empty remote version — skipping"; exit 0; } + +LOCAL_VER="$(cat "$VERSION_FILE" 2>/dev/null || echo none)" +if [ "$REMOTE_VER" = "$LOCAL_VER" ]; then + log "up to date ($LOCAL_VER)"; exit 0 +fi +log "update available: $LOCAL_VER -> $REMOTE_VER" + +# 2. Download + verify checksum +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT +curl -fsSL "${AUTH[@]}" "$DL/remoterig" -o "$TMP/remoterig" +curl -fsSL "${AUTH[@]}" "$DL/remoterig.sha256" -o "$TMP/remoterig.sha256" +( cd "$TMP" && echo "$(cat remoterig.sha256) remoterig" | sha256sum -c - ) || { + log "checksum FAILED — aborting update"; exit 1; } + +# 3. Deploy via the existing backup/restart/rollback logic +chmod +x "$TMP/remoterig" +"$DEPLOY_DIR/deploy.sh" "$TMP/remoterig" "$DEPLOY_PATH" "$SERVICE" + +# 4. Record the installed version +echo "$REMOTE_VER" > "$VERSION_FILE" +log "updated to $REMOTE_VER" diff --git a/scripts/remoterig-update.service b/scripts/remoterig-update.service new file mode 100644 index 0000000..5a1bf98 --- /dev/null +++ b/scripts/remoterig-update.service @@ -0,0 +1,10 @@ +[Unit] +Description=RemoteRig pull updater (checks Gitea dev release) +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/opt/remoterig/pi-update.sh +# Updater needs root to write the binary and restart the service +User=root diff --git a/scripts/remoterig-update.timer b/scripts/remoterig-update.timer new file mode 100644 index 0000000..972a600 --- /dev/null +++ b/scripts/remoterig-update.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Periodically check for RemoteRig updates (Gitea dev release) + +[Timer] +OnBootSec=2min +OnUnitActiveSec=5min +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/scripts/setup-pi.sh b/scripts/setup-pi.sh index 6571381..d43395d 100755 --- a/scripts/setup-pi.sh +++ b/scripts/setup-pi.sh @@ -204,6 +204,54 @@ else fi fi +# --------------------------------------------------------------------------- +# 6b. Install pull updater (Pi polls the Gitea dev release and self-updates) +# --------------------------------------------------------------------------- +info "Installing pull updater..." + +# deploy.sh + pi-update.sh live in the deploy dir (the updater calls them) +for f in deploy.sh pi-update.sh; do + if [ -f "${SCRIPT_DIR}/${f}" ]; then + cp "${SCRIPT_DIR}/${f}" "${DEPLOY_DIR}/${f}" + chmod +x "${DEPLOY_DIR}/${f}" + ok "Installed ${DEPLOY_DIR}/${f}" + else + warn "${SCRIPT_DIR}/${f} not found — skipping" + fi +done + +# update.env template (don't clobber an existing one that may hold a token) +UPDATE_ENV="${DEPLOY_DIR}/update.env" +if [ -f "${UPDATE_ENV}" ]; then + skip "${UPDATE_ENV} already exists (not overwriting)" +else + cat > "${UPDATE_ENV}" <<'ENVEOF' +# RemoteRig updater config +GITEA_BASE=https://code.cubecraftcreations.com +REPO=CubeCraft-Creations/remote-rig +# Read token — required only if the repo is private: +GITEA_TOKEN= +ENVEOF + chmod 600 "${UPDATE_ENV}" + ok "Wrote ${UPDATE_ENV} (set GITEA_TOKEN if the repo is private)" +fi + +# Updater service + timer +for unit in remoterig-update.service remoterig-update.timer; do + if [ -f "${SCRIPT_DIR}/${unit}" ]; then + cp "${SCRIPT_DIR}/${unit}" "/etc/systemd/system/${unit}" + ok "Installed ${unit}" + else + warn "${SCRIPT_DIR}/${unit} not found — skipping" + fi +done +systemctl daemon-reload +if systemctl enable --now remoterig-update.timer 2>/dev/null; then + ok "remoterig-update.timer enabled and started" +else + warn "Could not enable remoterig-update.timer" +fi + # --------------------------------------------------------------------------- # 7. Set static IP on wlan0 # --------------------------------------------------------------------------- @@ -317,20 +365,21 @@ echo " Setup complete!" echo "==============================================" echo " Mosquitto: $(systemctl is-active mosquitto 2>/dev/null || echo 'unknown')" echo " Service: ${SERVICE_NAME} (systemctl status ${SERVICE_NAME})" +echo " Updater: remoterig-update.timer (systemctl status remoterig-update.timer)" echo " Deploy dir: ${DEPLOY_DIR}" echo " Static IP: ${STATIC_IP} on wlan0" echo "" -echo " Next steps:" -echo " 1. Build the remoterig binary for ARM64:" -echo " GOOS=linux GOARCH=arm64 go build -o remoterig ./cmd/server" -echo " 2. Copy binary to Pi:" -echo " scp remoterig pi@192.168.8.56:/opt/remoterig/" -echo " 3. Copy config if needed:" -echo " scp config.yaml pi@192.168.8.56:/opt/remoterig/" -echo " 4. Start the service:" -echo " sudo systemctl start remoterig" -echo " 5. Check health:" -echo " curl http://192.168.8.56:8080/health" +echo " Deploys are pull-based: push to 'dev' on Gitea -> CI builds the" +echo " arm64 binary -> the Pi's timer pulls + installs it automatically." echo "" -echo " To deploy updates, use: scripts/deploy.sh" +echo " Next steps:" +echo " 1. If the repo is private, set a read token:" +echo " sudo sed -i 's/^GITEA_TOKEN=.*/GITEA_TOKEN=/' ${DEPLOY_DIR}/update.env" +echo " 2. Trigger / wait for an update check:" +echo " sudo systemctl start remoterig-update.service" +echo " journalctl -u remoterig-update.service -n 30" +echo " 3. Check health once deployed:" +echo " curl http://${STATIC_IP%/*}:8080/health" +echo "" +echo " Manual one-off deploy (local binary) still works: scripts/deploy.sh" echo "=============================================="