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