From c4f876f56b27fc1ce36e2dd45b40bd6df1416dfd Mon Sep 17 00:00:00 2001 From: Reed Chan Date: Thu, 31 Jul 2025 17:50:38 +0800 Subject: [PATCH 1/3] feat: multiarch build support --- .../docker-manifest-create-push/action.yml | 29 +++++ .github/workflows/build-multiarch.yml | 121 ++++++++++++++++++ Appium/Dockerfile | 2 +- 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 .github/actions/docker-manifest-create-push/action.yml create mode 100644 .github/workflows/build-multiarch.yml diff --git a/.github/actions/docker-manifest-create-push/action.yml b/.github/actions/docker-manifest-create-push/action.yml new file mode 100644 index 0000000..cb60150 --- /dev/null +++ b/.github/actions/docker-manifest-create-push/action.yml @@ -0,0 +1,29 @@ +name: "Docker Manifest Create & Push" +description: "Create OCI manifest list for multi-arch images and push to registry" +author: "babelcloud" +inputs: + manifest-list: + description: "Final manifest list name (e.g. ghcr.io/owner/repo:tag)" + required: true + manifests: + description: "Comma separated list of image references to include" + required: true + post-clean: + description: "Remove manifest list locally after push" + required: false + default: "false" +runs: + using: "composite" + steps: + - shell: bash + run: | + set -euo pipefail + MANIFEST_LIST="${{ inputs.manifest-list }}" + MANIFESTS="${{ inputs.manifests }}" + IFS=',' read -ra IMAGES <<< "$MANIFESTS" + echo "📦 Creating manifest list $MANIFEST_LIST with: ${IMAGES[*]}" + docker manifest create "$MANIFEST_LIST" "${IMAGES[@]}" + docker manifest push "$MANIFEST_LIST" + if [[ "${{ inputs.post-clean }}" == "true" ]]; then + docker manifest rm "$MANIFEST_LIST" || true + fi diff --git a/.github/workflows/build-multiarch.yml b/.github/workflows/build-multiarch.yml new file mode 100644 index 0000000..b19e565 --- /dev/null +++ b/.github/workflows/build-multiarch.yml @@ -0,0 +1,121 @@ +name: Build & Publish Multi-Arch Appium Image + +on: + # Manual trigger (custom tag optional) + workflow_dispatch: + inputs: + tag: + description: "Image tag (default: commit SHA)" + type: string + required: false + default: "" + push: + description: "Push image to registry" + type: boolean + required: false + default: false + push: + branches: ["**"] + tags: ["*"] + pull_request: + +jobs: + prepare: + runs-on: ubuntu-24.04 + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + tag: ${{ steps.set-tag.outputs.tag }} + steps: + - uses: actions/checkout@v4 + + - id: set-tag + name: Compute image tag + run: | + TAG_INPUT='${{ github.event.inputs.tag }}' + if [[ -n "$TAG_INPUT" ]]; then + TAG="$TAG_INPUT" + elif [[ "${{ github.ref }}" == refs/tags/* ]]; then + TAG="${GITHUB_REF##*/}" + else + TAG="$(git rev-parse --short HEAD)" + fi + echo "tag=$TAG" | tee -a "$GITHUB_OUTPUT" + + - id: set-matrix + name: Generate build matrix + run: | + TAG='${{ steps.set-tag.outputs.tag }}' + MATRIX=$(jq -nc --arg tag "$TAG" '{include:[{"arch":"amd64","platform":"linux/amd64","suffix":"-amd64","tag":$tag},{"arch":"arm64","platform":"linux/arm64","suffix":"-arm64","tag":$tag}]}') + echo "matrix=$MATRIX" | tee -a "$GITHUB_OUTPUT" + + build: + needs: prepare + name: Build (${{ matrix.arch }}) + runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm64' || 'ubuntu-24.04' }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.prepare.outputs.matrix) }} + permissions: + contents: read + packages: write + env: + REGISTRY: ghcr.io + IMAGE_NAME: babelcloud/appium + steps: + - uses: actions/checkout@v4 + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.GH_TOKEN_USER }} + password: ${{ secrets.GH_TOKEN }} + + - id: tag-list + name: Generate architecture-specific tags + run: | + FULL_TAG="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.tag }}${{ matrix.suffix }}" + echo "list=$FULL_TAG" | tee -a "$GITHUB_OUTPUT" + + - name: Build & (optional) Push + uses: docker/build-push-action@v5 + with: + context: ./Appium + file: ./Appium/Dockerfile + platforms: ${{ matrix.platform }} + tags: ${{ steps.tag-list.outputs.list }} + push: true + provenance: false + cache-from: type=registry,ref=ghcr.io/babelcloud/appium:buildcache-${{ matrix.arch }} + cache-to: type=registry,ref=ghcr.io/babelcloud/appium:buildcache-${{ matrix.arch }},mode=max + + manifest: + needs: [prepare, build] + if: ${{ success() && ((github.event_name == 'workflow_dispatch' && github.event.inputs.push == 'true') || startsWith(github.ref, 'refs/tags/')) }} + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + env: + REGISTRY: ghcr.io + IMAGE_NAME: babelcloud/appium + TAG: ${{ needs.prepare.outputs.tag }} + steps: + - uses: actions/checkout@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.GH_TOKEN_USER }} + password: ${{ secrets.GH_TOKEN }} + + - name: Create & Push Manifest + uses: ./.github/actions/docker-manifest-create-push + with: + manifest-list: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG }} + manifests: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG }}-amd64,${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG }}-arm64 + post-clean: true diff --git a/Appium/Dockerfile b/Appium/Dockerfile index a873300..1da41ec 100644 --- a/Appium/Dockerfile +++ b/Appium/Dockerfile @@ -48,7 +48,7 @@ RUN apt-get -qqy update && \ #=============== # Set JAVA_HOME #=============== -ENV JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64" \ +ENV JAVA_HOME="/usr/lib/jvm/java-11-openjdk-${TARGETARCH:-amd64}" \ PATH=$PATH:$JAVA_HOME/bin #=============================== From 6b4c85e1805033dae2cdc7a7329c257d3a86f6a4 Mon Sep 17 00:00:00 2001 From: Reed Chan Date: Fri, 1 Aug 2025 01:25:29 +0800 Subject: [PATCH 2/3] fix: build error; add Makefile --- .github/workflows/build-multiarch.yml | 15 ++++++++++----- Appium/Dockerfile | 3 ++- Makefile | 27 +++++++++++++++++++++++++++ app.sh | 19 +++++++++++++++++-- 4 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 Makefile diff --git a/.github/workflows/build-multiarch.yml b/.github/workflows/build-multiarch.yml index b19e565..1c68a89 100644 --- a/.github/workflows/build-multiarch.yml +++ b/.github/workflows/build-multiarch.yml @@ -1,7 +1,6 @@ -name: Build & Publish Multi-Arch Appium Image +name: Multi-Arch Appium Image Builder on: - # Manual trigger (custom tag optional) workflow_dispatch: inputs: tag: @@ -51,9 +50,9 @@ jobs: build: needs: prepare name: Build (${{ matrix.arch }}) - runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm64' || 'ubuntu-24.04' }} + runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} strategy: - fail-fast: false + fail-fast: true matrix: ${{ fromJson(needs.prepare.outputs.matrix) }} permissions: contents: read @@ -80,7 +79,7 @@ jobs: FULL_TAG="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.tag }}${{ matrix.suffix }}" echo "list=$FULL_TAG" | tee -a "$GITHUB_OUTPUT" - - name: Build & (optional) Push + - name: Build and push uses: docker/build-push-action@v5 with: context: ./Appium @@ -88,9 +87,15 @@ jobs: platforms: ${{ matrix.platform }} tags: ${{ steps.tag-list.outputs.list }} push: true + build-args: | + TARGETARCH=${{ matrix.arch }} provenance: false cache-from: type=registry,ref=ghcr.io/babelcloud/appium:buildcache-${{ matrix.arch }} cache-to: type=registry,ref=ghcr.io/babelcloud/appium:buildcache-${{ matrix.arch }},mode=max + labels: | + org.opencontainers.image.source=https://github.com/babelcloud/appium + annotations: | + org.opencontainers.image.source=https://github.com/babelcloud/appium manifest: needs: [prepare, build] diff --git a/Appium/Dockerfile b/Appium/Dockerfile index 1da41ec..0847607 100644 --- a/Appium/Dockerfile +++ b/Appium/Dockerfile @@ -48,7 +48,8 @@ RUN apt-get -qqy update && \ #=============== # Set JAVA_HOME #=============== -ENV JAVA_HOME="/usr/lib/jvm/java-11-openjdk-${TARGETARCH:-amd64}" \ +ARG TARGETARCH=amd64 +ENV JAVA_HOME="/usr/lib/jvm/java-17-openjdk-${TARGETARCH}" \ PATH=$PATH:$JAVA_HOME/bin #=============================== diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5a32e82 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +.PHONY: help build buildx +.DEFAULT_GOAL := help + +BASE_TAG := main +TAG := latest +NO_CACHE := false +PROGRESS := auto + +help: ## Print help message + @printf "\nUsage: make \n" + @grep -F -h "##" $(MAKEFILE_LIST) | grep -F -v grep -F | sed -e 's/\\$$//' | awk 'BEGIN {FS = ":*[[:space:]]*##"}; \ + { \ + if($$2 == "") \ + pass; \ + else if($$0 ~ /^#/) \ + printf "\n%s\n", $$2; \ + else if($$1 == "") \ + printf " %-28s%s\n", "", $$2; \ + else \ + printf " \033[34m%-28s\033[0m %s\n", $$1, $$2; \ + }' + +build: ## Build image locally + @bash app.sh build $(TAG) + +buildx: ## Build image for multiple architectures and push to registry + @bash app.sh buildx $(TAG) diff --git a/app.sh b/app.sh index cea77f3..0bdfd3f 100755 --- a/app.sh +++ b/app.sh @@ -1,6 +1,6 @@ #!/bin/bash -IMAGE="appium/appium" +IMAGE="ghcr.io/babelcloud/appium" if [ -z "$1" ]; then read -p "Task (test|build|push) : " TASK @@ -17,7 +17,19 @@ fi function build() { echo "Build docker image with version \"${VER}\"" docker build --no-cache -t ${IMAGE}:${VER} -f Appium/Dockerfile Appium - docker images + docker images ${IMAGE}:${VER} +} + +function buildx() { + echo "Build docker image with version \"${VER}\"" + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --push \ + --annotation org.opencontainers.image.source=https://github.com/babelcloud/appium \ + -t ${IMAGE}:${VER} \ + -f Appium/Dockerfile \ + Appium + docker images ${IMAGE}:${VER} } function test() { @@ -36,6 +48,9 @@ case $TASK in build) build ;; +buildx) + buildx + ;; test) test ;; From ca8465e1b59cdb2a3fc20fa0e5d18413bfb08223 Mon Sep 17 00:00:00 2001 From: Reed Chan Date: Fri, 1 Aug 2025 01:50:02 +0800 Subject: [PATCH 3/3] feat: add latest tag when push to main --- .github/workflows/build-multiarch.yml | 31 +++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-multiarch.yml b/.github/workflows/build-multiarch.yml index 1c68a89..75ca766 100644 --- a/.github/workflows/build-multiarch.yml +++ b/.github/workflows/build-multiarch.yml @@ -24,6 +24,7 @@ jobs: outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} tag: ${{ steps.set-tag.outputs.tag }} + is_main: ${{ steps.set-tag.outputs.is_main }} steps: - uses: actions/checkout@v4 @@ -40,6 +41,11 @@ jobs: fi echo "tag=$TAG" | tee -a "$GITHUB_OUTPUT" + # detect main branch + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "is_main=true" | tee -a "$GITHUB_OUTPUT" + fi + - id: set-matrix name: Generate build matrix run: | @@ -60,6 +66,7 @@ jobs: env: REGISTRY: ghcr.io IMAGE_NAME: babelcloud/appium + IS_MAIN: ${{ needs.prepare.outputs.is_main }} steps: - uses: actions/checkout@v4 @@ -73,11 +80,15 @@ jobs: username: ${{ secrets.GH_TOKEN_USER }} password: ${{ secrets.GH_TOKEN }} - - id: tag-list + - id: tags name: Generate architecture-specific tags run: | - FULL_TAG="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.tag }}${{ matrix.suffix }}" - echo "list=$FULL_TAG" | tee -a "$GITHUB_OUTPUT" + IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" + TAGS="$IMAGE:${{ matrix.tag }}${{ matrix.suffix }}" + if [[ "$IS_MAIN" == "true" ]]; then + TAGS="$TAGS,$IMAGE:latest${{ matrix.suffix }}" + fi + echo "tags=$TAGS" | tee -a "$GITHUB_OUTPUT" - name: Build and push uses: docker/build-push-action@v5 @@ -85,7 +96,7 @@ jobs: context: ./Appium file: ./Appium/Dockerfile platforms: ${{ matrix.platform }} - tags: ${{ steps.tag-list.outputs.list }} + tags: ${{ steps.tags.outputs.tags }} push: true build-args: | TARGETARCH=${{ matrix.arch }} @@ -99,7 +110,6 @@ jobs: manifest: needs: [prepare, build] - if: ${{ success() && ((github.event_name == 'workflow_dispatch' && github.event.inputs.push == 'true') || startsWith(github.ref, 'refs/tags/')) }} runs-on: ubuntu-24.04 permissions: contents: read @@ -107,6 +117,7 @@ jobs: env: REGISTRY: ghcr.io IMAGE_NAME: babelcloud/appium + IS_MAIN: ${{ needs.prepare.outputs.is_main }} TAG: ${{ needs.prepare.outputs.tag }} steps: - uses: actions/checkout@v4 @@ -118,9 +129,17 @@ jobs: username: ${{ secrets.GH_TOKEN_USER }} password: ${{ secrets.GH_TOKEN }} - - name: Create & Push Manifest + - name: Create & Push Manifest (SHA) uses: ./.github/actions/docker-manifest-create-push with: manifest-list: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG }} manifests: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG }}-amd64,${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG }}-arm64 post-clean: true + + - name: Create & Push Manifest (latest) + if: ${{ env.IS_MAIN == 'true' }} + uses: ./.github/actions/docker-manifest-create-push + with: + manifest-list: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + manifests: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-amd64,${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-arm64 + post-clean: true