From 988d6d5bb5554a6b1bc3d8f0d27b1a0007c96ece Mon Sep 17 00:00:00 2001
From: Hongze Gao <15101764808@163.com>
Date: Tue, 27 Jan 2026 15:51:20 +0800
Subject: [PATCH 1/2] ci: migrate API build to single-arch + manifest
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
• Add auto-arch local build script (script/demo)
• Unify tag scheme: -amd64 / -arm64
Signed-off-by: Hongze Gao <15101764808@163.com>
---
.github/workflows/deploy-api.yml | 143 ++++++++++++------------
api/config/environments/demo.rb | 1 -
script/demo/README.md | 70 ++++++++++++
script/demo/build-images-arm64-local.sh | 92 +++++++++++++++
4 files changed, 235 insertions(+), 71 deletions(-)
create mode 100644 script/demo/README.md
create mode 100644 script/demo/build-images-arm64-local.sh
diff --git a/.github/workflows/deploy-api.yml b/.github/workflows/deploy-api.yml
index dd4a17d..f384204 100644
--- a/.github/workflows/deploy-api.yml
+++ b/.github/workflows/deploy-api.yml
@@ -4,40 +4,37 @@ on:
workflow_call:
env:
- FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
- DEPLOY_QUEUE_THREAD_ID: suwdx19o6mqj
+ REGISTRY_ALIAS: m8q5m4u3
+ REPOSITORY: mega
+ IMAGE_TAG_BASE: campsite-0.1.0-pre-release
RUBY_VERSION: 3.3.4
NODE_VERSION: 18.16.1
BUNDLER_VERSION: 2.3.14
+# Grant least-privilege permissions
+permissions:
+ id-token: write
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
- deploy-api:
+ # Build single-arch images for each platform in parallel and push to ECR Public
+ build-push-single-arch:
runs-on: ubuntu-latest
strategy:
matrix:
- include:
- - cluster: gitmega-com
- service: campsite-api-staging-service-njx3hjyx
- - cluster: gitmono-com-mega-app
- service: campsite-api-service
+ platform: [linux/amd64, linux/arm64]
timeout-minutes: 30
steps:
- # - name: GitHub App token
- # id: gen-token
- # uses: tibdex/github-app-token@v2.1.0
- # with:
- # app_id: 1157102
- # private_key: ${{ secrets.DEPLOYBOT_TOKEN }}
-
- uses: actions/checkout@v4
- # with:
- # token: ${{ steps.gen-token.outputs.token }}
- # - name: Set git info
- # id: git-info
- # run: .github/set-git-message
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
- - name: Configure AWS Credentials
+ - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
@@ -50,17 +47,65 @@ jobs:
with:
registry-type: public
- - name: Build, tag, and push docker image to Amazon ECR Public
- working-directory: api
+ - name: Build & push image for ${{ matrix.platform }}
env:
REGISTRY: ${{ steps.login-ecr-public.outputs.registry }}
- REGISTRY_ALIAS: m8q5m4u3
- REPOSITORY: mega
- IMAGE_TAG: campsite-0.1.0-pre-release
+ PLATFORM: ${{ matrix.platform }}
run: |
- docker build -t $REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:$IMAGE_TAG . --build-arg RUBY_VERSION=${{env.RUBY_VERSION}} --build-arg NODE_VERSION=${{env.NODE_VERSION}} --build-arg BUNDLER_VERSION=${{env.BUNDLER_VERSION}}
- docker push $REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:$IMAGE_TAG
+ set -euo pipefail
+ IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}"
+ ARCH_SUFFIX=$(echo "$PLATFORM" | awk -F'/' '{print $2}')
+ docker buildx build \
+ --platform "$PLATFORM" \
+ --provenance=false \
+ --sbom=false \
+ -t "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}-$ARCH_SUFFIX" \
+ --build-arg RUBY_VERSION=${{ env.RUBY_VERSION }} \
+ --build-arg NODE_VERSION=${{ env.NODE_VERSION }} \
+ --build-arg BUNDLER_VERSION=${{ env.BUNDLER_VERSION }} \
+ -f ./api/Dockerfile \
+ --push ./api
+
+ # Create multi-arch manifest list
+ manifest:
+ needs: build-push-single-arch
+ runs-on: ubuntu-latest
+ steps:
+ - name: Configure AWS credentials
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ aws-region: us-east-1
+
+ - name: Login to Amazon ECR Public
+ id: login-ecr-public
+ uses: aws-actions/amazon-ecr-login@v2
+ with:
+ registry-type: public
+ - name: Create & push manifest list
+ env:
+ REGISTRY: ${{ steps.login-ecr-public.outputs.registry }}
+ run: |
+ IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}"
+ docker manifest create "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}" \
+ "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}-amd64" \
+ "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}-arm64"
+ docker manifest push "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}"
+
+ # Redeploy ECS services on two clusters using same image tag
+ deploy:
+ needs: manifest
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ include:
+ - cluster: gitmega-com
+ service: campsite-api-staging-service-njx3hjyx
+ - cluster: gitmono-com-mega-app
+ service: campsite-api-service
+ steps:
- name: Force ECS redeploy
run: |
aws ecs update-service \
@@ -71,45 +116,3 @@ jobs:
AWS_REGION: ap-southeast-2
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
-
- # - name: Setup Fly
- # uses: superfly/flyctl-actions/setup-flyctl@master
- # - name: Deploy API
- # working-directory: api
- # run: flyctl deploy --remote-only -e RELEASE_SHA=${{ github.sha }}
-
- # - name: Bump version and push tag
- # id: tag
- # uses: campsite/github-tag-action@master
- # with:
- # github_token: ${{ steps.gen-token.outputs.token }}
- # tag_prefix: api-
- # default_bump: major
- # fetch_all_tags: true
-
- # - name: Notify success
- # uses: campsite/campsite-github-action@v1.0.0
- # with:
- # api_key: ${{ secrets.CAMPBOT_API_KEY }}
- # action_type: create_message
- # thread_id: ${{ env.DEPLOY_QUEUE_THREAD_ID }}
- # content: |
- # **🛤️ API deployed**
-
- # [${{ github.actor }}](https://github.com/${{ github.actor }}) deployed ${{ steps.git-info.outputs.escaped-git-message }}
-
- # [${{ steps.git-info.outputs.git-sha }}](https://github.com/campsite/campsite/commit/${{ github.sha }}) · [Run logs](https://github.com/campsite/campsite/actions/runs/${{ github.run_id }})
-
- # - name: Notify failure
- # if: ${{ failure() }}
- # uses: campsite/campsite-github-action@v1.0.0
- # with:
- # api_key: ${{ secrets.CAMPBOT_API_KEY }}
- # action_type: create_message
- # thread_id: ${{ env.DEPLOY_QUEUE_THREAD_ID }}
- # content: |
- # **🚨 API deploy failed!**
-
- # [${{ github.actor }}](https://github.com/${{ github.actor }}) tried to deploy ${{ steps.git-info.outputs.escaped-git-message }}
-
- # [${{ steps.git-info.outputs.git-sha }}](https://github.com/campsite/campsite/commit/${{ github.sha }}) · [Run logs](https://github.com/campsite/campsite/actions/runs/${{ github.run_id }})
diff --git a/api/config/environments/demo.rb b/api/config/environments/demo.rb
index 8bacdff..5c7ec45 100644
--- a/api/config/environments/demo.rb
+++ b/api/config/environments/demo.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "active_support/core_ext/integer/time"
-require "debug/open_nonstop" unless Sidekiq.server?
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
diff --git a/script/demo/README.md b/script/demo/README.md
new file mode 100644
index 0000000..a5617bb
--- /dev/null
+++ b/script/demo/README.md
@@ -0,0 +1,70 @@
+# build-images-arm64-local.sh
+
+Build and optionally push a **single-architecture** Docker image for the Campsite API.
+The script automatically detects your host CPU and chooses the matching platform (`linux/amd64` or `linux/arm64`).
+A multi-arch manifest is **not** created here – that happens later in the CI workflow.
+
+## Features
+
+* Automatic architecture detection (`uname -m`).
+* Supports **build-only** (default) or **build & push** via `--push` flag.
+* Adds an architecture suffix (`-amd64` or `-arm64`) to the provided tag.
+* Pass-through build arguments for Ruby, Node and Bundler versions.
+
+## Prerequisites
+
+1. **Docker Buildx** enabled:
+ ```bash
+ docker buildx create --use # once per machine
+ ```
+2. If you plan to push (`--push`), log in to the target registry, e.g. ECR Public:
+ ```bash
+ aws ecr-public get-login-password --region us-east-1 | \
+ docker login --username AWS --password-stdin public.ecr.aws
+ ```
+
+## Usage
+
+```bash
+./script/demo/build-images-arm64-local.sh [IMAGE_TAG] [--push]
+```
+
+* `IMAGE_TAG` (optional) – base tag without architecture suffix.
+ Defaults to `campsite-0.1.0-pre-release`.
+* `--push` (optional) – push the image to the registry instead of only loading it locally.
+
+### Examples
+
+Build locally for the current architecture:
+```bash
+./script/demo/build-images-arm64-local.sh
+```
+
+Build with a custom tag:
+```bash
+./script/demo/build-images-arm64-local.sh my-feature-branch
+```
+
+Build and push:
+```bash
+./script/demo/build-images-arm64-local.sh release-1.2.3 --push
+```
+
+The resulting image names follow this pattern:
+```
+public.ecr.aws/m8q5m4u3/mega:-
+```
+where `` is `amd64` or `arm64`.
+
+## How It Works
+
+1. Parses CLI flags and determines whether to push.
+2. Detects host architecture and sets `--platform` and suffix.
+3. Invokes `docker buildx build` with exactly **one** platform, ensuring a single-architecture image.
+4. If `--push` is provided, the image is pushed; otherwise it is simply loaded into the local Docker daemon.
+
+## Notes
+
+* This script must be run from the repository root (or adjust the `api` build context path).
+* Multi-architecture support is handled in GitHub Actions where the single-arch images are combined into a manifest list.
+
diff --git a/script/demo/build-images-arm64-local.sh b/script/demo/build-images-arm64-local.sh
new file mode 100644
index 0000000..decf557
--- /dev/null
+++ b/script/demo/build-images-arm64-local.sh
@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+# Build a single-architecture Docker image for Campsite locally.
+# The script auto-detects host CPU (arm64 vs amd64) and builds the matching
+# architecture image. Multi-arch manifest is created later in CI.
+#
+# Usage:
+# ./script/demo/build-images-arm64-local.sh [IMAGE_TAG] [--push]
+# --push Also push to remote registry; if omitted, image is only built and loaded locally.
+#
+# Requirements:
+# - Docker with buildx enabled ("docker buildx create --use")
+# - If using --push you must already be logged into the destination registry.
+
+set -euo pipefail
+
+REGISTRY_ALIAS="m8q5m4u3" # Public ECR alias
+REGISTRY="public.ecr.aws/${REGISTRY_ALIAS}"
+REPOSITORY="mega"
+
+# ----------------------------------------
+# Parse CLI arguments
+# ----------------------------------------
+DO_PUSH=false
+POSITIONAL=()
+for arg in "$@"; do
+ case "$arg" in
+ --push)
+ DO_PUSH=true
+ ;;
+ *)
+ POSITIONAL+=("$arg")
+ ;;
+ esac
+done
+set -- "${POSITIONAL[@]}"
+
+# Image tag via first positional arg, or env, or default
+IMAGE_TAG_BASE="${IMAGE_TAG:-${1:-campsite-0.1.0-pre-release}}"
+
+# Detect host architecture and map to Docker platform / suffix
+ARCH_NATIVE="$(uname -m)"
+case "$ARCH_NATIVE" in
+ arm64|aarch64)
+ PLATFORM="linux/arm64"
+ ARCH_SUFFIX="arm64"
+ ;;
+ x86_64|amd64)
+ PLATFORM="linux/amd64"
+ ARCH_SUFFIX="amd64"
+ ;;
+ *)
+ echo "Unsupported architecture: $ARCH_NATIVE" >&2
+ exit 1
+ ;;
+esac
+
+# Build-time args (override via env vars if needed)
+RUBY_VERSION="${RUBY_VERSION:-3.3.4}"
+NODE_VERSION="${NODE_VERSION:-18.16.1}"
+BUNDLER_VERSION="${BUNDLER_VERSION:-2.3.14}"
+
+IMAGE_TAG="${IMAGE_TAG_BASE}-${ARCH_SUFFIX}"
+IMAGE_NAME="${REGISTRY}/${REPOSITORY}:${IMAGE_TAG}"
+
+echo "Building ${IMAGE_NAME} (${PLATFORM}) …"
+
+# Ensure buildx is available
+if ! docker buildx version >/dev/null 2>&1; then
+ echo "docker buildx not available; please install Docker 19.03+ and enable buildx." >&2
+ exit 1
+fi
+
+# Decide whether to push or just load locally
+if $DO_PUSH; then
+ PUSH_FLAG="--push"
+ echo "Will push image after build."
+else
+ PUSH_FLAG="--load" # keep locally
+ echo "Build only (no push)."
+fi
+
+# Build image
+docker buildx build \
+ --platform "$PLATFORM" \
+ $PUSH_FLAG \
+ -t "$IMAGE_NAME" \
+ api \
+ --build-arg RUBY_VERSION="$RUBY_VERSION" \
+ --build-arg NODE_VERSION="$NODE_VERSION" \
+ --build-arg BUNDLER_VERSION="$BUNDLER_VERSION"
+
+echo "Image ready: $IMAGE_NAME"
From 04e6d42434a70c7f0a8a06c89f7ac95a78c3f4e5 Mon Sep 17 00:00:00 2001
From: Hongze Gao <15101764808@163.com>
Date: Tue, 27 Jan 2026 17:09:25 +0800
Subject: [PATCH 2/2] ci: build amd64 in CI and merge manifest with locally
pushed arm64
- Refactor deploy-api workflow to build/push amd64 only
- Create manifest list from CI amd64 + locally pushed arm64, then deploy to 2 ECS services
- Update local build script to always --load and use docker push (avoid OCI index on buildx --push)
Signed-off-by: Hongze Gao <15101764808@163.com>
---
.github/workflows/deploy-api.yml | 21 +++++----------
script/demo/build-images-arm64-local.sh | 34 +++++++++++++++----------
2 files changed, 27 insertions(+), 28 deletions(-)
diff --git a/.github/workflows/deploy-api.yml b/.github/workflows/deploy-api.yml
index f384204..317c4a2 100644
--- a/.github/workflows/deploy-api.yml
+++ b/.github/workflows/deploy-api.yml
@@ -11,7 +11,6 @@ env:
NODE_VERSION: 18.16.1
BUNDLER_VERSION: 2.3.14
-# Grant least-privilege permissions
permissions:
id-token: write
contents: read
@@ -21,12 +20,9 @@ concurrency:
cancel-in-progress: true
jobs:
- # Build single-arch images for each platform in parallel and push to ECR Public
- build-push-single-arch:
+ # Build amd64 image in CI and push to ECR Public
+ build-push-amd64:
runs-on: ubuntu-latest
- strategy:
- matrix:
- platform: [linux/amd64, linux/arm64]
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
@@ -47,28 +43,26 @@ jobs:
with:
registry-type: public
- - name: Build & push image for ${{ matrix.platform }}
+ - name: Build & push amd64 image
env:
REGISTRY: ${{ steps.login-ecr-public.outputs.registry }}
- PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}"
- ARCH_SUFFIX=$(echo "$PLATFORM" | awk -F'/' '{print $2}')
docker buildx build \
- --platform "$PLATFORM" \
+ --platform linux/amd64 \
--provenance=false \
--sbom=false \
- -t "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}-$ARCH_SUFFIX" \
+ -t "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}-amd64" \
--build-arg RUBY_VERSION=${{ env.RUBY_VERSION }} \
--build-arg NODE_VERSION=${{ env.NODE_VERSION }} \
--build-arg BUNDLER_VERSION=${{ env.BUNDLER_VERSION }} \
-f ./api/Dockerfile \
--push ./api
- # Create multi-arch manifest list
+ # Merge locally-built arm64 image (pushed separately) with CI-built amd64
manifest:
- needs: build-push-single-arch
+ needs: build-push-amd64
runs-on: ubuntu-latest
steps:
- name: Configure AWS credentials
@@ -94,7 +88,6 @@ jobs:
"$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}-arm64"
docker manifest push "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}"
- # Redeploy ECS services on two clusters using same image tag
deploy:
needs: manifest
runs-on: ubuntu-latest
diff --git a/script/demo/build-images-arm64-local.sh b/script/demo/build-images-arm64-local.sh
index decf557..e2c289d 100644
--- a/script/demo/build-images-arm64-local.sh
+++ b/script/demo/build-images-arm64-local.sh
@@ -70,23 +70,29 @@ if ! docker buildx version >/dev/null 2>&1; then
exit 1
fi
-# Decide whether to push or just load locally
-if $DO_PUSH; then
- PUSH_FLAG="--push"
- echo "Will push image after build."
-else
- PUSH_FLAG="--load" # keep locally
- echo "Build only (no push)."
-fi
-
-# Build image
-docker buildx build \
+# Build image (always --load to ensure single-arch manifest)
+if ! docker buildx build \
--platform "$PLATFORM" \
- $PUSH_FLAG \
+ --load \
-t "$IMAGE_NAME" \
api \
--build-arg RUBY_VERSION="$RUBY_VERSION" \
--build-arg NODE_VERSION="$NODE_VERSION" \
- --build-arg BUNDLER_VERSION="$BUNDLER_VERSION"
+ --build-arg BUNDLER_VERSION="$BUNDLER_VERSION"; then
+ echo "docker buildx build failed" >&2
+ exit 1
+fi
-echo "Image ready: $IMAGE_NAME"
+echo "Image built and loaded locally: $IMAGE_NAME"
+
+# Optionally push to registry
+if $DO_PUSH; then
+ echo "Pushing $IMAGE_NAME to registry …"
+ if ! docker push "$IMAGE_NAME"; then
+ echo "docker push failed" >&2
+ exit 1
+ fi
+ echo "Image pushed successfully: $IMAGE_NAME"
+else
+ echo "Push flag not set; skipping docker push."
+fi