diff --git a/Makefile b/Makefile index 0390bd06..0e9896a4 100644 --- a/Makefile +++ b/Makefile @@ -2,14 +2,15 @@ BRANCH := $(shell git rev-parse --abbrev-ref HEAD) BUILDDATE := $(shell date -u +%FT%T%z) BUILDTS := $(shell date -u +%s) REVISION := $(shell git rev-parse HEAD) -VERSION_DEV := 0.5.0-dev$(shell date +%Y%m%d%H%M) -VERSION := 0.4.9 +VERSION := 0.5.0 +VERSION_DEV := $(VERSION)-dev$(shell date -u +%Y%m%d%H%M) PROMETHEUS_TAG := github.com/prometheus/common/version KVM_PKG_NAME := github.com/jetkvm/kvm BUILDKIT_FLAVOR := arm-rockchip830-linux-uclibcgnueabihf BUILDKIT_PATH ?= /opt/jetkvm-native-buildkit +DOCKER_BUILD_TAG ?= ghcr.io/jetkvm/buildkit:latest SKIP_NATIVE_IF_EXISTS ?= 0 SKIP_UI_BUILD ?= 0 ENABLE_SYNC_TRACE ?= 0 @@ -37,7 +38,7 @@ ifneq ($(wildcard $(BUILDKIT_PATH)),) CGO_LDFLAGS="-L$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/lib -L$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/sysroot/usr/lib -lrockit -lrockchip_mpp -lrga -lpthread -lm" \ CC="$(BUILDKIT_PATH)/bin/$(BUILDKIT_FLAVOR)-gcc" \ LD="$(BUILDKIT_PATH)/bin/$(BUILDKIT_FLAVOR)-ld" \ - CGO_ENABLED=1 + CGO_ENABLED=1 # GO_RELEASE_BUILD_ARGS := $(GO_RELEASE_BUILD_ARGS) -x -work endif @@ -47,6 +48,14 @@ BIN_DIR := $(shell pwd)/bin TEST_DIRS := $(shell find . -name "*_test.go" -type f -exec dirname {} \; | sort -u) +test: + go test ./... + +lint: + go vet ./... + +check: lint test + build_native: @if [ "$(SKIP_NATIVE_IF_EXISTS)" = "1" ] && [ -f "internal/native/cgo/lib/libjknative.a" ]; then \ echo "libjknative.a already exists, skipping native build..."; \ @@ -58,7 +67,21 @@ build_native: ./scripts/build_cgo.sh; \ fi -build_dev: build_native +# NOTE: VERSION_DEV must be explicitly passed to nested make invocations. +# VERSION_DEV contains $(shell date ...) which gets re-evaluated when a new make +# process starts. Without passing it explicitly, a minute boundary crossed during +# the build would cause version mismatch between what's displayed and what's built. +build_dev: + @if [ ! -d "$(BUILDKIT_PATH)" ]; then \ + echo "Toolchain not found, running build_dev in Docker..."; \ + rm -rf internal/native/cgo/build; \ + docker run --rm -v "$$(pwd):/build" \ + $(DOCKER_BUILD_TAG) make _build_dev_inner VERSION_DEV=$(VERSION_DEV); \ + else \ + $(MAKE) _build_dev_inner VERSION_DEV=$(VERSION_DEV); \ + fi + +_build_dev_inner: build_native @echo "Building..." $(GO_CMD) build \ -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \ @@ -114,26 +137,119 @@ frontend: \) -exec sh -c 'gzip -9 -kfv {}' \; ;\ fi -dev_release: frontend build_dev - @echo "Uploading release... $(VERSION_DEV)" +git_check_dev: + @if [ "$$(git rev-parse --abbrev-ref HEAD)" != "dev" ]; then \ + echo "Error: Must be on 'dev' branch"; exit 1; \ + fi + @if [ -n "$$(git status --porcelain)" ]; then \ + echo "Error: Working tree is dirty. Commit or stash changes."; exit 1; \ + fi + @git fetch origin dev + @if [ "$$(git rev-parse HEAD)" != "$$(git rev-parse origin/dev)" ]; then \ + echo "Error: Local dev is not up-to-date with origin/dev"; exit 1; \ + fi + @command -v gh >/dev/null 2>&1 || { echo "Error: gh CLI not installed"; exit 1; } + @gh auth status >/dev/null 2>&1 || { echo "Error: gh CLI not authenticated. Run 'gh auth login'"; exit 1; } + +dev_release: git_check_dev + @echo "═══════════════════════════════════════════════════════" + @echo " DEV Release" + @echo "═══════════════════════════════════════════════════════" + @echo " Version: $(VERSION_DEV)" + @echo " Tag: release/$(VERSION_DEV)" + @echo " Branch: $$(git rev-parse --abbrev-ref HEAD)" + @echo " Commit: $$(git rev-parse --short HEAD)" + @echo " Time: $$(date -u +%FT%T%z)" + @echo "═══════════════════════════════════════════════════════" + @read -p "Proceed? [y/N] " confirm && [ "$$confirm" = "y" ] || exit 1 + $(MAKE) check frontend build_dev + @read -p "Test on device before release? [y/N] " test_confirm; \ + if [ "$$test_confirm" = "y" ]; then \ + read -p "Device IP: " device_ip; \ + ./scripts/test_release_on_device.sh "$$device_ip" bin/jetkvm_app test $(VERSION_DEV) || exit 1; \ + fi + @echo "Uploading device app to R2..." @shasum -a 256 bin/jetkvm_app | cut -d ' ' -f 1 > bin/jetkvm_app.sha256 rclone copyto bin/jetkvm_app r2://jetkvm-update/app/$(VERSION_DEV)/jetkvm_app rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION_DEV)/jetkvm_app.sha256 + ./scripts/deploy_cloud_app.sh -v $(VERSION_DEV) --skip-confirmation + @git tag release/$(VERSION_DEV) + @git push origin release/$(VERSION_DEV) + gh release create release/$(VERSION_DEV) bin/jetkvm_app bin/jetkvm_app.sha256 --prerelease --generate-notes + @echo "✓ Released: release/$(VERSION_DEV)" -build_release: frontend build_native +# NOTE: VERSION is passed explicitly for consistency with build_dev (see comment above). +# While VERSION is static, passing it explicitly ensures the pattern is consistent +# and prevents issues if VERSION ever becomes dynamic. +build_release: + @if [ ! -d "$(BUILDKIT_PATH)" ]; then \ + echo "Toolchain not found, running build_release in Docker..."; \ + rm -rf internal/native/cgo/build; \ + docker run --rm -v "$$(pwd):/build" \ + $(DOCKER_BUILD_TAG) make _build_release_inner VERSION=$(VERSION); \ + else \ + $(MAKE) _build_release_inner VERSION=$(VERSION); \ + fi + +_build_release_inner: build_native @echo "Building release..." $(GO_CMD) build \ -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" \ $(GO_RELEASE_BUILD_ARGS) \ -o bin/jetkvm_app cmd/main.go -release: - @if rclone lsf r2://jetkvm-update/app/$(VERSION)/ | grep -q "jetkvm_app"; then \ - echo "Error: Version $(VERSION) already exists. Please update the VERSION variable."; \ - exit 1; \ +release: git_check_dev + @if rclone lsf r2://jetkvm-update/app/$(VERSION)/ 2>/dev/null | grep -q "jetkvm_app"; then \ + echo "Error: Version $(VERSION) already exists in R2"; exit 1; \ fi - make build_release - @echo "Uploading release..." + @latest_dev=$$(curl -s "https://api.jetkvm.com/releases?deviceId=123&prerelease=true" | jq -r '.appVersion // ""'); \ + if ! echo "$$latest_dev" | grep -q "^$(VERSION)-dev"; then \ + echo ""; \ + echo "⚠️ Warning: No dev release found for $(VERSION)"; \ + echo " Latest pre-release: $$latest_dev"; \ + echo ""; \ + read -p "Release production without prior dev release? [y/N] " confirm && [ "$$confirm" = "y" ] || exit 1; \ + fi + @echo "═══════════════════════════════════════════════════════" + @echo " PRODUCTION Release" + @echo "═══════════════════════════════════════════════════════" + @echo " Version: $(VERSION)" + @echo " Tag: release/$(VERSION)" + @echo " Branch: $$(git rev-parse --abbrev-ref HEAD)" + @echo " Commit: $$(git rev-parse --short HEAD)" + @echo " Time: $$(date -u +%FT%T%z)" + @echo "═══════════════════════════════════════════════════════" + @read -p "Proceed with PRODUCTION release? [y/N] " confirm && [ "$$confirm" = "y" ] || exit 1 + $(MAKE) check frontend build_release + @read -p "Test on device before release? [y/N] " test_confirm; \ + if [ "$$test_confirm" = "y" ]; then \ + read -p "Device IP: " device_ip; \ + ./scripts/test_release_on_device.sh "$$device_ip" bin/jetkvm_app test $(VERSION) || exit 1; \ + fi + @echo "Uploading device app to R2..." @shasum -a 256 bin/jetkvm_app | cut -d ' ' -f 1 > bin/jetkvm_app.sha256 rclone copyto bin/jetkvm_app r2://jetkvm-update/app/$(VERSION)/jetkvm_app - rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION)/jetkvm_app.sha256 \ No newline at end of file + rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION)/jetkvm_app.sha256 + ./scripts/deploy_cloud_app.sh -v $(VERSION) --set-as-default --skip-confirmation + @git tag release/$(VERSION) + @git push origin release/$(VERSION) + gh release create release/$(VERSION) bin/jetkvm_app bin/jetkvm_app.sha256 --generate-notes + @echo "" + @echo "✓ Released: release/$(VERSION)" + @echo "" + @echo "Next: Run 'make bump-version' to prepare for next release cycle" + +bump-version: + @next_default=$$(echo $(VERSION) | awk -F. '{print $$1"."$$2"."$$3+1}'); \ + echo "Current version: $(VERSION)"; \ + read -p "Next version [$$next_default]: " next_ver; \ + next_ver=$${next_ver:-$$next_default}; \ + if ! echo "$$next_ver" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$$'; then \ + echo "Error: Invalid version '$$next_ver'. Must be semver format (e.g., 1.2.3)"; \ + exit 1; \ + fi; \ + sed -i 's/^VERSION := .*/VERSION := '"$$next_ver"'/' Makefile && \ + git add Makefile && \ + git commit -m "Bump version to $$next_ver" && \ + git push && \ + echo "✓ Bumped to $$next_ver" diff --git a/scripts/deploy_cloud_app.sh b/scripts/deploy_cloud_app.sh index e54d61c8..b6c3fe84 100755 --- a/scripts/deploy_cloud_app.sh +++ b/scripts/deploy_cloud_app.sh @@ -4,113 +4,66 @@ set -e SCRIPT_PATH=$(realpath "$(dirname $(realpath "${BASH_SOURCE[0]}"))") source ${SCRIPT_PATH}/build_utils.sh -function show_help() { - echo "Usage: $0 [options]" - echo "Options:" - echo " -b, --branch Checkout branch" - echo " --set-as-default Set as default" - echo " --skip-confirmation Skip confirmation" - echo " --help Show help" -} - -# Parse command line arguments -CHECKOUT_BRANCH= +VERSION= SET_AS_DEFAULT=false SKIP_CONFIRMATION=false + while [[ $# -gt 0 ]]; do case $1 in - -b|--branch) - CHECKOUT_BRANCH="$2" - shift 2 - ;; - --set-as-default) - SET_AS_DEFAULT=true - shift - ;; - --skip-confirmation) - SKIP_CONFIRMATION=true - shift - ;; + -v|--version) VERSION="$2"; shift 2 ;; + --set-as-default) SET_AS_DEFAULT=true; shift ;; + --skip-confirmation) SKIP_CONFIRMATION=true; shift ;; --help) - show_help - exit 0 - ;; - *) - echo "Unknown option: $1" - show_help - exit 1 - ;; + echo "Usage: $0 -v VERSION [--set-as-default] [--skip-confirmation]" + echo " -v VERSION Version to deploy (required)" + echo " --set-as-default Also deploy to root (production only)" + echo " --skip-confirmation Skip confirmation prompt" + exit 0 ;; + *) echo "Unknown option: $1"; exit 1 ;; esac done - -# Checkout current branch in a new temporary directory -# only popd when exiting the script -TMP_DIR=$(mktemp -d) -trap 'popd > /dev/null && rm -rf ${TMP_DIR}' EXIT -msg_info "Copying repository to a new temporary directory ${TMP_DIR} ..." -# git fetch origin ${CH}ECKOUT_BRANCH:${CHECKOUT_BRANCH} -git clone . ${TMP_DIR} -cp ${SCRIPT_PATH}/versioned.patch ${TMP_DIR} -msg_info "Checking out branch ${CHECKOUT_BRANCH} ..." -pushd ${TMP_DIR} > /dev/null -git checkout ${CHECKOUT_BRANCH} - - -# CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) -# # Verify branch name matches release/x.x.x or release/x.x.x-dev... -# if [[ ! $CURRENT_BRANCH =~ ^(release|release-cloud-app)/[0-9]+\.[0-9]+\.[0-9]+(-dev[0-9]+)?$ ]]; then -# msg_err "Current branch '$CURRENT_BRANCH' does not match required pattern" -# msg_err "Expected: release/x.x.x OR release/x.x.x-dev20241104123632" -# exit 1 -# fi - -CURRENT_BRANCH=release/0.5.0 +[ -z "$VERSION" ] && { msg_err "Version required. Use -v VERSION"; exit 1; } GIT_COMMIT=$(git rev-parse HEAD) BUILD_TIMESTAMP=$(date -u +%FT%T%z) -VERSION=${CURRENT_BRANCH#release/} -VERSION=${VERSION#release-cloud-app/} -if [[ ! $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+(-dev[0-9]+)?$ ]]; then - msg_err "Version '$VERSION' does not match required pattern" - msg_err "Expected: x.x.x OR x.x.x-dev20241104123632" - exit 1 -fi -# Change to ui directory cd ui - -if [ "$SET_AS_DEFAULT" = true ]; then - # Build for root dist - msg_info "Building for root dist..." - npm ci - npm run build:prod -fi - -# Build for versioned dist/v/VERSION -msg_info "Building for dist/v/${VERSION}..." npm ci + +# Build versioned app +msg_info "Building cloud app /v/${VERSION}/..." npm run build:prod -- --base=/v/${VERSION}/ --outDir dist/v/${VERSION} -# Ask for confirmation -if [ "$SKIP_CONFIRMATION" = false ]; then -read -p "Do you want to deploy the cloud app to production? (y/N): " -n 1 -r -echo "" - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - msg_err "Deployment cancelled." - exit 0 - fi +# Build root app if --set-as-default +if [ "$SET_AS_DEFAULT" = true ]; then + msg_info "Building root cloud app..." + npm run build:prod -- --outDir dist/root fi -# Deploy to production -msg_info "Deploying to r2://jetkvm-cloud-app..." -rclone copyto \ - --progress \ - --stats=1s \ +# Confirmation +if [ "$SKIP_CONFIRMATION" = false ]; then + read -p "Deploy cloud app v${VERSION}? [y/N] " -n 1 -r + echo "" + [[ $REPLY =~ ^[Yy]$ ]] || { msg_err "Cancelled."; exit 0; } +fi + +# Deploy versioned +msg_info "Deploying /v/${VERSION}/ to r2://jetkvm-cloud-app/v/${VERSION}..." +rclone copyto --progress \ --header-upload="x-amz-meta-jetkvm-version: ${VERSION}" \ --header-upload="x-amz-meta-jetkvm-build-ref: ${GIT_COMMIT}" \ --header-upload="x-amz-meta-jetkvm-build-timestamp: ${BUILD_TIMESTAMP}" \ - dist \ -r2://jetkvm-cloud-app + dist/v/${VERSION} r2://jetkvm-cloud-app/v/${VERSION} -msg_ok "Successfully deployed v${VERSION} to production" +# Deploy root if --set-as-default +if [ "$SET_AS_DEFAULT" = true ]; then + msg_info "Deploying root to r2://jetkvm-cloud-app..." + rclone copyto --progress \ + --header-upload="x-amz-meta-jetkvm-version: ${VERSION}" \ + --header-upload="x-amz-meta-jetkvm-build-ref: ${GIT_COMMIT}" \ + --header-upload="x-amz-meta-jetkvm-build-timestamp: ${BUILD_TIMESTAMP}" \ + dist/root r2://jetkvm-cloud-app +fi + +msg_ok "Deployed cloud app v${VERSION}" diff --git a/scripts/test_release_on_device.sh b/scripts/test_release_on_device.sh new file mode 100755 index 00000000..66ad4f52 --- /dev/null +++ b/scripts/test_release_on_device.sh @@ -0,0 +1,64 @@ +#!/bin/bash +set -e + +DEVICE_IP="$1" +BINARY_PATH="$2" +ACTION="$3" # "deploy", "restore", or "test" +VERSION="$4" # required for "test" action + +REMOTE_USER="root" +REMOTE_BIN_PATH="/userdata/jetkvm/bin" +REMOTE_UPDATE_PATH="/userdata/jetkvm" +SSH_OPTS="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o ConnectTimeout=10" + +ssh_cmd() { ssh $SSH_OPTS "${REMOTE_USER}@${DEVICE_IP}" "$@"; } + +case "$ACTION" in + deploy) + echo "Backing up current binary..." + ssh_cmd "cp ${REMOTE_BIN_PATH}/jetkvm_app ${REMOTE_BIN_PATH}/jetkvm_app.pre_release_backup 2>/dev/null || true" + echo "Deploying new binary via OTA update mechanism..." + ssh_cmd "cat > ${REMOTE_UPDATE_PATH}/jetkvm_app.update" < "$BINARY_PATH" + echo "Rebooting device..." + ssh_cmd "reboot" || true + ;; + restore) + echo "Restoring backup..." + ssh_cmd "cp ${REMOTE_BIN_PATH}/jetkvm_app.pre_release_backup ${REMOTE_BIN_PATH}/jetkvm_app" + echo "Rebooting device..." + ssh_cmd "reboot" || true + ;; + test) + # Full interactive test flow + [ -z "$VERSION" ] && { echo "Error: VERSION required for test action"; exit 1; } + + echo "" + echo "Deploying $VERSION to $DEVICE_IP..." + "$0" "$DEVICE_IP" "$BINARY_PATH" deploy + + echo "" + echo "═══════════════════════════════════════════════════════" + echo " Device is rebooting. Please verify:" + echo "═══════════════════════════════════════════════════════" + echo " Expected version: $VERSION" + echo " Settings page: http://$DEVICE_IP/settings/general" + echo "" + echo " Check that the version shown in the UI matches above." + echo "═══════════════════════════════════════════════════════" + echo "" + read -p "Does the version match and binary work correctly? [y/n] " works + + echo "Restoring device to previous binary..." + "$0" "$DEVICE_IP" "$BINARY_PATH" restore + + if [ "$works" != "y" ]; then + echo "Test failed." + exit 1 + fi + echo "Test passed." + ;; + *) + echo "Usage: $0 [version]" + exit 1 + ;; +esac