docker-publish-daily-beta.sh 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  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 with that version, overwriting any
  16. # previous image with the same tag. Optionally creates/updates a GitHub prerelease.
  17. #
  18. # Beta versions are never tagged as 'latest'. Users update by pulling the same tag
  19. # (e.g., docker pull ghcr.io/maziggy/bambuddy:0.2.2b1) or using Watchtower.
  20. #
  21. # Prerequisites:
  22. # 1. Log in to ghcr.io:
  23. # echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_USERNAME --password-stdin
  24. #
  25. # 2. Log in to Docker Hub:
  26. # docker login -u YOUR_USERNAME
  27. #
  28. # 3. GitHub CLI (gh) authenticated for creating releases
  29. #
  30. # Supported architectures:
  31. # - linux/amd64 (x86_64, most servers/desktops)
  32. # - linux/arm64 (Raspberry Pi 4/5, Apple Silicon via emulation)
  33. set -e
  34. # Configuration
  35. GHCR_REGISTRY="ghcr.io"
  36. DOCKERHUB_REGISTRY="docker.io"
  37. IMAGE_NAME="maziggy/bambuddy"
  38. GHCR_IMAGE="${GHCR_REGISTRY}/${IMAGE_NAME}"
  39. DOCKERHUB_IMAGE="${DOCKERHUB_REGISTRY}/${IMAGE_NAME}"
  40. PLATFORMS="linux/amd64,linux/arm64"
  41. BUILDER_NAME="bambuddy-builder"
  42. CONFIG_FILE="backend/app/core/config.py"
  43. CHANGELOG_FILE="CHANGELOG.md"
  44. # Colors for output
  45. RED='\033[0;31m'
  46. GREEN='\033[0;32m'
  47. YELLOW='\033[1;33m'
  48. BLUE='\033[0;34m'
  49. NC='\033[0m' # No Color
  50. # Parse arguments
  51. PARALLEL=false
  52. PUSH_GHCR=true
  53. PUSH_DOCKERHUB=true
  54. SKIP_RELEASE=false
  55. for arg in "$@"; do
  56. case $arg in
  57. --parallel)
  58. PARALLEL=true
  59. ;;
  60. --ghcr-only)
  61. PUSH_DOCKERHUB=false
  62. ;;
  63. --dockerhub-only)
  64. PUSH_GHCR=false
  65. ;;
  66. --skip-release)
  67. SKIP_RELEASE=true
  68. ;;
  69. --help|-h)
  70. echo "Usage: $0 [--parallel] [--ghcr-only] [--dockerhub-only] [--skip-release]"
  71. echo ""
  72. echo "Build and publish a daily beta Docker image using the APP_VERSION from config.py."
  73. echo ""
  74. echo "Options:"
  75. echo " --parallel Build both architectures simultaneously"
  76. echo " --ghcr-only Only push to GitHub Container Registry"
  77. echo " --dockerhub-only Only push to Docker Hub"
  78. echo " --skip-release Build+push without creating/updating GitHub release"
  79. echo " --help, -h Show this help"
  80. exit 0
  81. ;;
  82. *)
  83. echo -e "${RED}Unknown argument: $arg${NC}"
  84. echo "Run $0 --help for usage"
  85. exit 1
  86. ;;
  87. esac
  88. done
  89. # ============================================================
  90. # Step 1: Read and validate APP_VERSION
  91. # ============================================================
  92. echo -e "${BLUE}[1/4] Validating APP_VERSION...${NC}"
  93. VERSION=$(grep -oP 'APP_VERSION = "\K[^"]+' "$CONFIG_FILE")
  94. if [ -z "$VERSION" ]; then
  95. echo -e "${RED}Error: Could not read APP_VERSION from ${CONFIG_FILE}${NC}"
  96. exit 1
  97. fi
  98. if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+b[0-9]+$ ]]; then
  99. echo -e "${RED}Error: APP_VERSION '${VERSION}' is not a beta version (expected X.Y.Zb<N>)${NC}"
  100. exit 1
  101. fi
  102. echo -e "${GREEN} APP_VERSION: ${VERSION}${NC}"
  103. # ============================================================
  104. # Step 2: Build & push Docker images
  105. # ============================================================
  106. echo ""
  107. # Get CPU count
  108. CPU_COUNT=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
  109. echo -e "${GREEN}================================================${NC}"
  110. echo -e "${GREEN} Daily beta build${NC}"
  111. echo -e "${GREEN} Version: ${VERSION}${NC}"
  112. echo -e "${GREEN} Platforms: ${PLATFORMS}${NC}"
  113. echo -e "${GREEN} CPU cores: ${CPU_COUNT}${NC}"
  114. if [ "$PARALLEL" = true ]; then
  115. echo -e "${GREEN} Mode: PARALLEL (both archs simultaneously)${NC}"
  116. else
  117. echo -e "${GREEN} Mode: Sequential (amd64 → arm64)${NC}"
  118. fi
  119. echo -e "${GREEN} Registries:${NC}"
  120. if [ "$PUSH_GHCR" = true ]; then
  121. echo -e "${GREEN} - ${GHCR_IMAGE}${NC}"
  122. fi
  123. if [ "$PUSH_DOCKERHUB" = true ]; then
  124. echo -e "${GREEN} - ${DOCKERHUB_IMAGE}${NC}"
  125. fi
  126. echo -e "${GREEN}================================================${NC}"
  127. echo ""
  128. # Check registry logins
  129. if [ "$PUSH_GHCR" = true ]; then
  130. if ! grep -q "ghcr.io" ~/.docker/config.json 2>/dev/null; then
  131. echo -e "${YELLOW}Warning: You may not be logged in to ghcr.io${NC}"
  132. echo "Run: echo \$GITHUB_TOKEN | docker login ghcr.io -u YOUR_USERNAME --password-stdin"
  133. echo ""
  134. fi
  135. fi
  136. if [ "$PUSH_DOCKERHUB" = true ]; then
  137. if ! grep -q "index.docker.io\|docker.io" ~/.docker/config.json 2>/dev/null; then
  138. echo -e "${RED}Error: You are not logged in to Docker Hub${NC}"
  139. echo "Run: docker login -u YOUR_USERNAME"
  140. echo ""
  141. exit 1
  142. fi
  143. fi
  144. # Setup buildx builder if not exists
  145. echo -e "${BLUE}[2/4] Setting up Docker Buildx and building...${NC}"
  146. if ! docker buildx inspect "$BUILDER_NAME" >/dev/null 2>&1; then
  147. echo "Creating new buildx builder: $BUILDER_NAME (optimized for ${CPU_COUNT} cores)"
  148. docker buildx create \
  149. --name "$BUILDER_NAME" \
  150. --driver docker-container \
  151. --driver-opt network=host \
  152. --driver-opt "env.BUILDKIT_STEP_LOG_MAX_SIZE=10000000" \
  153. --buildkitd-flags "--allow-insecure-entitlement network.host --oci-worker-gc=false" \
  154. --config /dev/stdin <<EOF
  155. [worker.oci]
  156. max-parallelism = ${CPU_COUNT}
  157. EOF
  158. docker buildx inspect --bootstrap "$BUILDER_NAME"
  159. fi
  160. docker buildx use "$BUILDER_NAME"
  161. # Verify builder supports multi-platform
  162. if ! docker buildx inspect --bootstrap | grep -q "linux/arm64"; then
  163. echo -e "${YELLOW}Installing QEMU for cross-platform builds...${NC}"
  164. docker run --privileged --rm tonistiigi/binfmt --install all
  165. fi
  166. # Beta versions never get 'latest' tag
  167. echo -e "${YELLOW}Beta version — skipping 'latest' tag${NC}"
  168. # Build tags for all target registries
  169. TAGS=""
  170. if [ "$PUSH_GHCR" = true ]; then
  171. TAGS="$TAGS -t ${GHCR_IMAGE}:${VERSION}"
  172. fi
  173. if [ "$PUSH_DOCKERHUB" = true ]; then
  174. TAGS="$TAGS -t ${DOCKERHUB_IMAGE}:${VERSION}"
  175. fi
  176. # Common build args (no cache to ensure clean builds)
  177. BUILD_ARGS="--provenance=false --sbom=false --no-cache --pull"
  178. if [ "$PARALLEL" = true ]; then
  179. # Parallel build: Build each architecture separately then combine manifests
  180. echo -e "${YELLOW}Building amd64 and arm64 in parallel (${CPU_COUNT} cores each, no cache)...${NC}"
  181. # Build per-arch staging tags for each target registry
  182. ARCH_TAGS_AMD64=""
  183. ARCH_TAGS_ARM64=""
  184. if [ "$PUSH_GHCR" = true ]; then
  185. ARCH_TAGS_AMD64="$ARCH_TAGS_AMD64 -t ${GHCR_IMAGE}:${VERSION}-amd64"
  186. ARCH_TAGS_ARM64="$ARCH_TAGS_ARM64 -t ${GHCR_IMAGE}:${VERSION}-arm64"
  187. fi
  188. if [ "$PUSH_DOCKERHUB" = true ]; then
  189. ARCH_TAGS_AMD64="$ARCH_TAGS_AMD64 -t ${DOCKERHUB_IMAGE}:${VERSION}-amd64"
  190. ARCH_TAGS_ARM64="$ARCH_TAGS_ARM64 -t ${DOCKERHUB_IMAGE}:${VERSION}-arm64"
  191. fi
  192. # Build amd64 in background
  193. (
  194. echo -e "${BLUE}[amd64] Starting build...${NC}"
  195. docker buildx build \
  196. --platform linux/amd64 \
  197. ${ARCH_TAGS_AMD64} \
  198. ${BUILD_ARGS} \
  199. --push \
  200. . 2>&1 | sed 's/^/[amd64] /'
  201. echo -e "${GREEN}[amd64] Complete!${NC}"
  202. ) &
  203. PID_AMD64=$!
  204. # Build arm64 in background
  205. (
  206. echo -e "${BLUE}[arm64] Starting build...${NC}"
  207. docker buildx build \
  208. --platform linux/arm64 \
  209. ${ARCH_TAGS_ARM64} \
  210. ${BUILD_ARGS} \
  211. --push \
  212. . 2>&1 | sed 's/^/[arm64] /'
  213. echo -e "${GREEN}[arm64] Complete!${NC}"
  214. ) &
  215. PID_ARM64=$!
  216. # Wait for both builds
  217. echo "Waiting for parallel builds to complete..."
  218. wait $PID_AMD64
  219. wait $PID_ARM64
  220. # Create multi-arch manifests per registry (no cross-registry blob copies)
  221. echo -e "${BLUE}Creating multi-arch manifests...${NC}"
  222. if [ "$PUSH_GHCR" = true ]; then
  223. echo -e "${BLUE} Creating GHCR manifest...${NC}"
  224. docker buildx imagetools create \
  225. -t "${GHCR_IMAGE}:${VERSION}" \
  226. "${GHCR_IMAGE}:${VERSION}-amd64" \
  227. "${GHCR_IMAGE}:${VERSION}-arm64"
  228. fi
  229. if [ "$PUSH_DOCKERHUB" = true ]; then
  230. echo -e "${BLUE} Creating Docker Hub manifest...${NC}"
  231. docker buildx imagetools create \
  232. -t "${DOCKERHUB_IMAGE}:${VERSION}" \
  233. "${DOCKERHUB_IMAGE}:${VERSION}-amd64" \
  234. "${DOCKERHUB_IMAGE}:${VERSION}-arm64"
  235. fi
  236. else
  237. # Sequential build (default): Build both platforms in one command
  238. echo -e "${YELLOW}Building sequentially with ${CPU_COUNT} cores (no cache)...${NC}"
  239. DOCKER_BUILDKIT=1 docker buildx build \
  240. --platform "$PLATFORMS" \
  241. ${BUILD_ARGS} \
  242. $TAGS \
  243. --push \
  244. .
  245. fi
  246. # ============================================================
  247. # Step 3: Create/update GitHub release
  248. # ============================================================
  249. if [ "$SKIP_RELEASE" = true ]; then
  250. echo -e "${YELLOW}[3/4] Skipping GitHub release (--skip-release)${NC}"
  251. else
  252. echo -e "${BLUE}[3/4] Creating/updating GitHub release...${NC}"
  253. # Extract release notes from CHANGELOG: content between ## [<version>] and the next ## [ heading
  254. CHANGELOG_NOTES=$(sed -n "/^## \[${VERSION}\]/,/^## \[/{/^## \[/!p}" "$CHANGELOG_FILE" | sed '/^$/d; 1{/^$/d}')
  255. if [ -z "$CHANGELOG_NOTES" ]; then
  256. echo -e "${YELLOW} Warning: No changelog notes found for ${VERSION}${NC}"
  257. CHANGELOG_NOTES="No changelog notes available for this release."
  258. fi
  259. # Build pull commands for the release body
  260. PULL_COMMANDS=""
  261. if [ "$PUSH_GHCR" = true ]; then
  262. PULL_COMMANDS="docker pull ghcr.io/maziggy/bambuddy:${VERSION}"
  263. fi
  264. if [ "$PUSH_DOCKERHUB" = true ]; then
  265. if [ -n "$PULL_COMMANDS" ]; then
  266. PULL_COMMANDS="${PULL_COMMANDS}
  267. # or
  268. docker pull maziggy/bambuddy:${VERSION}"
  269. else
  270. PULL_COMMANDS="docker pull maziggy/bambuddy:${VERSION}"
  271. fi
  272. fi
  273. # Create the release body
  274. TODAY=$(date +%Y-%m-%d)
  275. RELEASE_BODY=$(cat <<EOF
  276. > [!NOTE]
  277. > This is a **daily beta build** (${TODAY}). It contains the latest fixes and improvements but may have undiscovered issues.
  278. >
  279. > **Docker users:** Update by pulling the new image:
  280. > \`\`\`
  281. > ${PULL_COMMANDS}
  282. > \`\`\`
  283. >
  284. > **Tip:** Use [Watchtower](https://containrrr.dev/watchtower/) to automatically update when new daily builds are pushed.
  285. ---
  286. ${CHANGELOG_NOTES}
  287. EOF
  288. )
  289. # Check if release already exists
  290. if gh release view "v${VERSION}" >/dev/null 2>&1; then
  291. echo " Updating existing release v${VERSION}..."
  292. gh release edit "v${VERSION}" \
  293. --title "Daily Beta Build v${VERSION} (${TODAY})" \
  294. --prerelease \
  295. --notes "$RELEASE_BODY"
  296. echo -e "${GREEN} Updated GitHub release: v${VERSION}${NC}"
  297. else
  298. echo " Creating new release v${VERSION}..."
  299. # Ensure the tag exists on remote
  300. if ! git ls-remote --tags origin "v${VERSION}" | grep -q "v${VERSION}"; then
  301. echo " Creating and pushing tag v${VERSION}..."
  302. if ! git rev-parse "v${VERSION}" >/dev/null 2>&1; then
  303. git tag "v${VERSION}"
  304. fi
  305. git push origin "v${VERSION}"
  306. fi
  307. gh release create "v${VERSION}" \
  308. --title "Daily Beta Build v${VERSION} (${TODAY})" \
  309. --prerelease \
  310. --notes "$RELEASE_BODY"
  311. echo -e "${GREEN} Created GitHub release: v${VERSION}${NC}"
  312. fi
  313. fi
  314. # ============================================================
  315. # Step 4: Verify
  316. # ============================================================
  317. echo -e "${BLUE}[4/4] Verifying...${NC}"
  318. if [ "$PUSH_GHCR" = true ]; then
  319. echo -e "${BLUE}GHCR manifest:${NC}"
  320. docker buildx imagetools inspect "${GHCR_IMAGE}:${VERSION}"
  321. fi
  322. if [ "$PUSH_DOCKERHUB" = true ]; then
  323. echo -e "${BLUE}Docker Hub manifest:${NC}"
  324. docker buildx imagetools inspect "${DOCKERHUB_IMAGE}:${VERSION}"
  325. fi
  326. if [ "$SKIP_RELEASE" != true ]; then
  327. echo ""
  328. echo -e "${BLUE}GitHub release:${NC}"
  329. gh release view "v${VERSION}"
  330. fi
  331. # ============================================================
  332. # Summary
  333. # ============================================================
  334. echo ""
  335. echo -e "${GREEN}================================================${NC}"
  336. echo -e "${GREEN} Daily beta build complete!${NC}"
  337. echo -e "${GREEN} Version: ${VERSION}${NC}"
  338. echo -e "${GREEN}================================================${NC}"
  339. if [ "$PUSH_GHCR" = true ]; then
  340. echo " GHCR: ${GHCR_IMAGE}:${VERSION}"
  341. fi
  342. if [ "$PUSH_DOCKERHUB" = true ]; then
  343. echo " Docker Hub: ${DOCKERHUB_IMAGE}:${VERSION}"
  344. fi
  345. if [ "$SKIP_RELEASE" != true ]; then
  346. echo " Release: https://github.com/${IMAGE_NAME}/releases/tag/v${VERSION}"
  347. fi
  348. echo ""
  349. echo -e "${BLUE}Supported platforms:${NC}"
  350. echo " - linux/amd64 (Intel/AMD servers, desktops)"
  351. echo " - linux/arm64 (Raspberry Pi 4/5, Apple Silicon)"
  352. echo ""
  353. echo -e "${GREEN}Users can now run:${NC}"
  354. if [ "$PUSH_GHCR" = true ]; then
  355. echo " docker pull ${GHCR_IMAGE}:${VERSION}"
  356. fi
  357. if [ "$PUSH_DOCKERHUB" = true ]; then
  358. echo " docker pull ${DOCKERHUB_IMAGE}:${VERSION}"
  359. echo " docker pull ${IMAGE_NAME}:${VERSION} # shorthand"
  360. fi