docker-publish-daily-beta.sh 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. #!/bin/bash
  2. # Daily beta build: build Docker image, push to registries, create/update GitHub prerelease
  3. #
  4. # Usage:
  5. # ./docker-publish-daily-beta.sh [--parallel] [--ghcr-only] [--dockerhub-only] [--skip-release]
  6. #
  7. # Examples:
  8. # ./docker-publish-daily-beta.sh # Full daily beta workflow
  9. # ./docker-publish-daily-beta.sh --parallel # Build both archs simultaneously
  10. # ./docker-publish-daily-beta.sh --ghcr-only # Only push to GHCR
  11. # ./docker-publish-daily-beta.sh --dockerhub-only # Only push to Docker Hub
  12. # ./docker-publish-daily-beta.sh --skip-release # Build+push without GitHub release
  13. #
  14. # Reads APP_VERSION from backend/app/core/config.py (must be a beta version like 0.2.2b1).
  15. # Builds and pushes a multi-arch Docker image tagged as 'daily'. Each push overwrites the
  16. # previous 'daily' image. A GitHub prerelease is created with a date-stamped tag for history.
  17. #
  18. # Users can stay up to date by pulling the 'daily' tag or using Watchtower
  19. # (https://watchtower.nickfedor.com, image 'nickfedor/watchtower'):
  20. # docker pull ghcr.io/maziggy/bambuddy:daily
  21. #
  22. # Prerequisites:
  23. # 1. Log in to ghcr.io:
  24. # echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_USERNAME --password-stdin
  25. #
  26. # 2. Log in to Docker Hub:
  27. # docker login -u YOUR_USERNAME
  28. #
  29. # 3. GitHub CLI (gh) authenticated for creating releases
  30. #
  31. # Supported architectures:
  32. # - linux/amd64 (x86_64, most servers/desktops)
  33. # - linux/arm64 (Raspberry Pi 4/5, Apple Silicon via emulation)
  34. set -e
  35. # Configuration
  36. GHCR_REGISTRY="ghcr.io"
  37. DOCKERHUB_REGISTRY="docker.io"
  38. IMAGE_NAME="maziggy/bambuddy"
  39. GHCR_IMAGE="${GHCR_REGISTRY}/${IMAGE_NAME}"
  40. DOCKERHUB_IMAGE="${DOCKERHUB_REGISTRY}/${IMAGE_NAME}"
  41. PLATFORMS="linux/amd64,linux/arm64"
  42. BUILDER_NAME="bambuddy-builder"
  43. CONFIG_FILE="backend/app/core/config.py"
  44. CHANGELOG_FILE="CHANGELOG.md"
  45. # Colors for output
  46. RED='\033[0;31m'
  47. GREEN='\033[0;32m'
  48. YELLOW='\033[1;33m'
  49. BLUE='\033[0;34m'
  50. NC='\033[0m' # No Color
  51. # Parse arguments
  52. PARALLEL=false
  53. PUSH_GHCR=true
  54. PUSH_DOCKERHUB=true
  55. SKIP_RELEASE=false
  56. INCLUDE_SIDECARS=false
  57. for arg in "$@"; do
  58. case $arg in
  59. --parallel)
  60. PARALLEL=true
  61. ;;
  62. --ghcr-only)
  63. PUSH_DOCKERHUB=false
  64. ;;
  65. --dockerhub-only)
  66. PUSH_GHCR=false
  67. ;;
  68. --skip-release)
  69. SKIP_RELEASE=true
  70. ;;
  71. --include-sidecars)
  72. INCLUDE_SIDECARS=true
  73. ;;
  74. --help|-h)
  75. echo "Usage: $0 [--parallel] [--ghcr-only] [--dockerhub-only] [--skip-release] [--include-sidecars]"
  76. echo ""
  77. echo "Build and publish a daily beta Docker image using the APP_VERSION from config.py."
  78. echo ""
  79. echo "Options:"
  80. echo " --parallel Build both architectures simultaneously"
  81. echo " --ghcr-only Only push to GitHub Container Registry"
  82. echo " --dockerhub-only Only push to Docker Hub"
  83. echo " --skip-release Build+push without creating/updating GitHub release"
  84. echo " --include-sidecars Also rebuild + push orca-slicer-api and bambu-studio-api"
  85. echo " images (off by default — slicer rebuilds are expensive)"
  86. echo " --help, -h Show this help"
  87. exit 0
  88. ;;
  89. *)
  90. echo -e "${RED}Unknown argument: $arg${NC}"
  91. echo "Run $0 --help for usage"
  92. exit 1
  93. ;;
  94. esac
  95. done
  96. # ============================================================
  97. # Step 1: Read and validate APP_VERSION
  98. # ============================================================
  99. echo -e "${BLUE}[1/4] Validating APP_VERSION...${NC}"
  100. VERSION=$(grep -oP 'APP_VERSION = "\K[^"]+' "$CONFIG_FILE")
  101. if [ -z "$VERSION" ]; then
  102. echo -e "${RED}Error: Could not read APP_VERSION from ${CONFIG_FILE}${NC}"
  103. exit 1
  104. fi
  105. if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+b[0-9]+$ ]]; then
  106. echo -e "${RED}Error: APP_VERSION '${VERSION}' is not a beta version (expected X.Y.Zb<N>)${NC}"
  107. exit 1
  108. fi
  109. # Date-stamped tag for GitHub releases only (not used as Docker tag)
  110. DAILY_DATE=$(date +%Y%m%d)
  111. DAILY_TAG="${VERSION}-daily.${DAILY_DATE}"
  112. echo -e "${GREEN} APP_VERSION: ${VERSION}${NC}"
  113. echo -e "${GREEN} Docker tag: daily${NC}"
  114. echo -e "${GREEN} Release tag: v${DAILY_TAG}${NC}"
  115. # ============================================================
  116. # Step 2: Build & push Docker images
  117. # ============================================================
  118. echo ""
  119. # Get CPU count
  120. CPU_COUNT=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
  121. echo -e "${GREEN}================================================${NC}"
  122. echo -e "${GREEN} Daily beta build${NC}"
  123. echo -e "${GREEN} Version: ${VERSION}${NC}"
  124. echo -e "${GREEN} Docker tag: daily${NC}"
  125. echo -e "${GREEN} Platforms: ${PLATFORMS}${NC}"
  126. echo -e "${GREEN} CPU cores: ${CPU_COUNT}${NC}"
  127. if [ "$PARALLEL" = true ]; then
  128. echo -e "${GREEN} Mode: PARALLEL (both archs simultaneously)${NC}"
  129. else
  130. echo -e "${GREEN} Mode: Sequential (amd64 → arm64)${NC}"
  131. fi
  132. echo -e "${GREEN} Registries:${NC}"
  133. if [ "$PUSH_GHCR" = true ]; then
  134. echo -e "${GREEN} - ${GHCR_IMAGE}${NC}"
  135. fi
  136. if [ "$PUSH_DOCKERHUB" = true ]; then
  137. echo -e "${GREEN} - ${DOCKERHUB_IMAGE}${NC}"
  138. fi
  139. echo -e "${GREEN}================================================${NC}"
  140. echo ""
  141. # Check registry logins
  142. if [ "$PUSH_GHCR" = true ]; then
  143. if ! grep -q "ghcr.io" ~/.docker/config.json 2>/dev/null; then
  144. echo -e "${YELLOW}Warning: You may not be logged in to ghcr.io${NC}"
  145. echo "Run: echo \$GITHUB_TOKEN | docker login ghcr.io -u YOUR_USERNAME --password-stdin"
  146. echo ""
  147. fi
  148. fi
  149. if [ "$PUSH_DOCKERHUB" = true ]; then
  150. if ! grep -q "index.docker.io\|docker.io" ~/.docker/config.json 2>/dev/null; then
  151. echo -e "${RED}Error: You are not logged in to Docker Hub${NC}"
  152. echo "Run: docker login -u YOUR_USERNAME"
  153. echo ""
  154. exit 1
  155. fi
  156. fi
  157. # Setup buildx builder if not exists
  158. echo -e "${BLUE}[2/4] Setting up Docker Buildx and building...${NC}"
  159. if ! docker buildx inspect "$BUILDER_NAME" >/dev/null 2>&1; then
  160. echo "Creating new buildx builder: $BUILDER_NAME (optimized for ${CPU_COUNT} cores)"
  161. docker buildx create \
  162. --name "$BUILDER_NAME" \
  163. --driver docker-container \
  164. --driver-opt network=host \
  165. --driver-opt "env.BUILDKIT_STEP_LOG_MAX_SIZE=10000000" \
  166. --buildkitd-flags "--allow-insecure-entitlement network.host --oci-worker-gc=false" \
  167. --config /dev/stdin <<EOF
  168. [worker.oci]
  169. max-parallelism = ${CPU_COUNT}
  170. EOF
  171. docker buildx inspect --bootstrap "$BUILDER_NAME"
  172. fi
  173. docker buildx use "$BUILDER_NAME"
  174. # Verify builder supports multi-platform
  175. if ! docker buildx inspect --bootstrap | grep -q "linux/arm64"; then
  176. echo -e "${YELLOW}Installing QEMU for cross-platform builds...${NC}"
  177. docker run --privileged --rm tonistiigi/binfmt --install all
  178. fi
  179. # Beta versions get 'daily' tag (never 'latest')
  180. echo -e "${YELLOW}Beta version — tagging as 'daily' (not 'latest')${NC}"
  181. # Build tags for all target registries
  182. TAGS=""
  183. if [ "$PUSH_GHCR" = true ]; then
  184. TAGS="$TAGS -t ${GHCR_IMAGE}:daily"
  185. fi
  186. if [ "$PUSH_DOCKERHUB" = true ]; then
  187. TAGS="$TAGS -t ${DOCKERHUB_IMAGE}:daily"
  188. fi
  189. # Common build args (no cache to ensure clean builds)
  190. BUILD_ARGS="--provenance=false --sbom=false --no-cache --pull"
  191. if [ "$PARALLEL" = true ]; then
  192. # Parallel build: Build each architecture separately then combine manifests
  193. echo -e "${YELLOW}Building amd64 and arm64 in parallel (${CPU_COUNT} cores each, no cache)...${NC}"
  194. # Build per-arch staging tags for each target registry
  195. ARCH_TAGS_AMD64=""
  196. ARCH_TAGS_ARM64=""
  197. if [ "$PUSH_GHCR" = true ]; then
  198. ARCH_TAGS_AMD64="$ARCH_TAGS_AMD64 -t ${GHCR_IMAGE}:daily-amd64"
  199. ARCH_TAGS_ARM64="$ARCH_TAGS_ARM64 -t ${GHCR_IMAGE}:daily-arm64"
  200. fi
  201. if [ "$PUSH_DOCKERHUB" = true ]; then
  202. ARCH_TAGS_AMD64="$ARCH_TAGS_AMD64 -t ${DOCKERHUB_IMAGE}:daily-amd64"
  203. ARCH_TAGS_ARM64="$ARCH_TAGS_ARM64 -t ${DOCKERHUB_IMAGE}:daily-arm64"
  204. fi
  205. # Build amd64 in background
  206. (
  207. echo -e "${BLUE}[amd64] Starting build...${NC}"
  208. docker buildx build \
  209. --platform linux/amd64 \
  210. ${ARCH_TAGS_AMD64} \
  211. ${BUILD_ARGS} \
  212. --push \
  213. . 2>&1 | sed 's/^/[amd64] /'
  214. echo -e "${GREEN}[amd64] Complete!${NC}"
  215. ) &
  216. PID_AMD64=$!
  217. # Build arm64 in background
  218. (
  219. echo -e "${BLUE}[arm64] Starting build...${NC}"
  220. docker buildx build \
  221. --platform linux/arm64 \
  222. ${ARCH_TAGS_ARM64} \
  223. ${BUILD_ARGS} \
  224. --push \
  225. . 2>&1 | sed 's/^/[arm64] /'
  226. echo -e "${GREEN}[arm64] Complete!${NC}"
  227. ) &
  228. PID_ARM64=$!
  229. # Wait for both builds
  230. echo "Waiting for parallel builds to complete..."
  231. wait $PID_AMD64
  232. wait $PID_ARM64
  233. # Create multi-arch manifests per registry (no cross-registry blob copies)
  234. echo -e "${BLUE}Creating multi-arch manifests...${NC}"
  235. if [ "$PUSH_GHCR" = true ]; then
  236. echo -e "${BLUE} Creating GHCR manifest...${NC}"
  237. docker buildx imagetools create \
  238. -t "${GHCR_IMAGE}:daily" \
  239. "${GHCR_IMAGE}:daily-amd64" \
  240. "${GHCR_IMAGE}:daily-arm64"
  241. fi
  242. if [ "$PUSH_DOCKERHUB" = true ]; then
  243. echo -e "${BLUE} Creating Docker Hub manifest...${NC}"
  244. docker buildx imagetools create \
  245. -t "${DOCKERHUB_IMAGE}:daily" \
  246. "${DOCKERHUB_IMAGE}:daily-amd64" \
  247. "${DOCKERHUB_IMAGE}:daily-arm64"
  248. fi
  249. else
  250. # Sequential build (default): Build both platforms in one command
  251. echo -e "${YELLOW}Building sequentially with ${CPU_COUNT} cores (no cache)...${NC}"
  252. DOCKER_BUILDKIT=1 docker buildx build \
  253. --platform "$PLATFORMS" \
  254. ${BUILD_ARGS} \
  255. $TAGS \
  256. --push \
  257. .
  258. fi
  259. # ============================================================
  260. # Step 3: Create/update GitHub release
  261. # ============================================================
  262. if [ "$SKIP_RELEASE" = true ]; then
  263. echo -e "${YELLOW}[3/4] Skipping GitHub release (--skip-release)${NC}"
  264. else
  265. echo -e "${BLUE}[3/4] Creating/updating GitHub release...${NC}"
  266. # Extract release notes from CHANGELOG: content between ## [<version>] and the next ## [ heading
  267. CHANGELOG_NOTES=$(sed -n "/^## \[${VERSION}\]/,/^## \[/{/^## \[/!p}" "$CHANGELOG_FILE" | sed '/^$/d; 1{/^$/d}')
  268. # Strip @mentions so GitHub doesn't auto-generate a "Contributors" section
  269. CHANGELOG_NOTES=$(echo "$CHANGELOG_NOTES" | sed 's/@\([a-zA-Z0-9_-]*\)/\1/g')
  270. if [ -z "$CHANGELOG_NOTES" ]; then
  271. echo -e "${YELLOW} Warning: No changelog notes found for ${VERSION}${NC}"
  272. CHANGELOG_NOTES="No changelog notes available for this release."
  273. fi
  274. # GitHub caps release body at 125000 chars. Reserve ~2000 for the boilerplate
  275. # header/pull-commands and truncate the changelog tail with a link to the full file.
  276. MAX_NOTES_LEN=122000
  277. if [ ${#CHANGELOG_NOTES} -gt $MAX_NOTES_LEN ]; then
  278. echo -e "${YELLOW} Changelog section is ${#CHANGELOG_NOTES} chars; truncating to ${MAX_NOTES_LEN} for GitHub release body${NC}"
  279. CHANGELOG_NOTES="${CHANGELOG_NOTES:0:$MAX_NOTES_LEN}
  280. ---
  281. _Changelog truncated — see the full [CHANGELOG.md](https://github.com/maziggy/bambuddy/blob/main/CHANGELOG.md) for the complete list._"
  282. fi
  283. # Build pull commands for the release body
  284. PULL_COMMANDS=""
  285. if [ "$PUSH_GHCR" = true ]; then
  286. PULL_COMMANDS="docker pull ghcr.io/maziggy/bambuddy:daily"
  287. fi
  288. if [ "$PUSH_DOCKERHUB" = true ]; then
  289. if [ -n "$PULL_COMMANDS" ]; then
  290. PULL_COMMANDS="${PULL_COMMANDS}
  291. # or
  292. docker pull maziggy/bambuddy:daily"
  293. else
  294. PULL_COMMANDS="docker pull maziggy/bambuddy:daily"
  295. fi
  296. fi
  297. # Create the release body
  298. TODAY=$(date +%Y-%m-%d)
  299. RELEASE_BODY=$(cat <<EOF
  300. > [!NOTE]
  301. > This is a **daily beta build** (${TODAY}). It contains the latest fixes and improvements but may have undiscovered issues.
  302. >
  303. > **Docker users:** Update by pulling the new image:
  304. > \`\`\`
  305. > ${PULL_COMMANDS}
  306. > \`\`\`
  307. >
  308. > **Tip:** Use [Watchtower](https://watchtower.nickfedor.com) (image \`nickfedor/watchtower\`) to automatically update when new daily builds are pushed.
  309. ---
  310. ${CHANGELOG_NOTES}
  311. EOF
  312. )
  313. # Delete ALL old daily releases — only the latest daily build should exist
  314. echo " Cleaning up old daily releases..."
  315. OLD_DAILY_RELEASES=$(gh release list --limit 100 --json tagName --jq '.[] | select(.tagName | test("-daily\\.")) | .tagName' 2>/dev/null || true)
  316. if [ -n "$OLD_DAILY_RELEASES" ]; then
  317. while IFS= read -r old_tag; do
  318. echo " Deleting old daily release: ${old_tag}..."
  319. gh release delete "$old_tag" --yes --cleanup-tag 2>/dev/null || true
  320. done <<< "$OLD_DAILY_RELEASES"
  321. fi
  322. # Create/move tag to current HEAD and push
  323. echo " Tagging current HEAD as v${DAILY_TAG}..."
  324. git tag -f "v${DAILY_TAG}"
  325. git push origin "v${DAILY_TAG}" --force
  326. echo " Creating release v${DAILY_TAG}..."
  327. NOTES_FILE=$(mktemp)
  328. trap 'rm -f "$NOTES_FILE"' EXIT
  329. printf '%s\n' "$RELEASE_BODY" > "$NOTES_FILE"
  330. gh release create "v${DAILY_TAG}" \
  331. --title "Daily Beta Build v${DAILY_TAG}" \
  332. --prerelease \
  333. --generate-notes=false \
  334. --notes-file "$NOTES_FILE"
  335. echo -e "${GREEN} Created GitHub release: v${DAILY_TAG}${NC}"
  336. fi
  337. # ============================================================
  338. # Step 4: Verify
  339. # ============================================================
  340. echo -e "${BLUE}[4/4] Verifying...${NC}"
  341. if [ "$PUSH_GHCR" = true ]; then
  342. echo -e "${BLUE}GHCR manifest:${NC}"
  343. docker buildx imagetools inspect "${GHCR_IMAGE}:daily"
  344. fi
  345. if [ "$PUSH_DOCKERHUB" = true ]; then
  346. echo -e "${BLUE}Docker Hub manifest:${NC}"
  347. docker buildx imagetools inspect "${DOCKERHUB_IMAGE}:daily"
  348. fi
  349. if [ "$SKIP_RELEASE" != true ]; then
  350. echo ""
  351. echo -e "${BLUE}GitHub release:${NC}"
  352. gh release view "v${DAILY_TAG}"
  353. fi
  354. # ============================================================
  355. # Summary
  356. # ============================================================
  357. echo ""
  358. echo -e "${GREEN}================================================${NC}"
  359. echo -e "${GREEN} Daily beta build complete!${NC}"
  360. echo -e "${GREEN} Version: ${VERSION}${NC}"
  361. echo -e "${GREEN}================================================${NC}"
  362. if [ "$PUSH_GHCR" = true ]; then
  363. echo " GHCR: ${GHCR_IMAGE}:daily"
  364. fi
  365. if [ "$PUSH_DOCKERHUB" = true ]; then
  366. echo " Docker Hub: ${DOCKERHUB_IMAGE}:daily"
  367. fi
  368. if [ "$SKIP_RELEASE" != true ]; then
  369. echo " Release: https://github.com/${IMAGE_NAME}/releases/tag/v${DAILY_TAG}"
  370. fi
  371. echo ""
  372. echo -e "${BLUE}Supported platforms:${NC}"
  373. echo " - linux/amd64 (Intel/AMD servers, desktops)"
  374. echo " - linux/arm64 (Raspberry Pi 4/5, Apple Silicon)"
  375. echo ""
  376. echo -e "${GREEN}Users can now run:${NC}"
  377. if [ "$PUSH_GHCR" = true ]; then
  378. echo " docker pull ${GHCR_IMAGE}:daily"
  379. fi
  380. if [ "$PUSH_DOCKERHUB" = true ]; then
  381. echo " docker pull ${DOCKERHUB_IMAGE}:daily"
  382. echo " docker pull ${IMAGE_NAME}:daily # shorthand"
  383. fi
  384. # ============================================================
  385. # Sidecar images (orca-slicer-api + bambu-studio-api) — opt-in for daily
  386. # ============================================================
  387. SIDECAR_SCRIPT="/opt/claude/projects/orca-slicer-api/docker-publish-sidecars.sh"
  388. if [ "$INCLUDE_SIDECARS" != true ]; then
  389. echo ""
  390. echo -e "${YELLOW}Sidecar images not rebuilt (daily default).${NC}"
  391. echo -e "${YELLOW}Pass --include-sidecars to also publish orca-slicer-api + bambu-studio-api.${NC}"
  392. elif [ ! -x "$SIDECAR_SCRIPT" ]; then
  393. echo ""
  394. echo -e "${YELLOW}Sidecar helper not found at ${SIDECAR_SCRIPT} — skipping sidecar build.${NC}"
  395. else
  396. echo ""
  397. echo -e "${GREEN}================================================${NC}"
  398. echo -e "${GREEN} Publishing sidecar images (daily channel)${NC}"
  399. echo -e "${GREEN}================================================${NC}"
  400. SIDECAR_ARGS="--channel daily --version ${VERSION}"
  401. [ "$PARALLEL" = true ] && SIDECAR_ARGS="$SIDECAR_ARGS --parallel"
  402. [ "$PUSH_GHCR" = false ] && SIDECAR_ARGS="$SIDECAR_ARGS --dockerhub-only"
  403. [ "$PUSH_DOCKERHUB" = false ] && SIDECAR_ARGS="$SIDECAR_ARGS --ghcr-only"
  404. "$SIDECAR_SCRIPT" $SIDECAR_ARGS
  405. fi