docker-publish-daily-beta.sh 15 KB

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