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..75ca766 --- /dev/null +++ b/.github/workflows/build-multiarch.yml @@ -0,0 +1,145 @@ +name: Multi-Arch Appium Image Builder + +on: + 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 }} + is_main: ${{ steps.set-tag.outputs.is_main }} + 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" + + # 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: | + 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-arm' || 'ubuntu-24.04' }} + strategy: + fail-fast: true + matrix: ${{ fromJson(needs.prepare.outputs.matrix) }} + permissions: + contents: read + packages: write + env: + REGISTRY: ghcr.io + IMAGE_NAME: babelcloud/appium + IS_MAIN: ${{ needs.prepare.outputs.is_main }} + 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: tags + name: Generate architecture-specific tags + run: | + 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 + with: + context: ./Appium + file: ./Appium/Dockerfile + platforms: ${{ matrix.platform }} + tags: ${{ steps.tags.outputs.tags }} + 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] + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + 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 + + - 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 (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 diff --git a/Appium/Dockerfile b/Appium/Dockerfile index a873300..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-17-openjdk-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 ;;