diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index 42f4d580b0135..03d8b7e424c43 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -13,6 +13,10 @@ inputs: required: true description: 'Deno version' type: string + arch: + required: false + description: 'Architecture' + default: 'amd64' service: required: false description: 'Container to build' @@ -46,7 +50,7 @@ runs: password: ${{ inputs.CR_PAT }} - name: Restore packages build - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v6 with: name: packages-build path: /tmp @@ -58,7 +62,7 @@ runs: - name: Restore meteor build if: inputs.service == 'rocketchat' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v6 with: name: build-${{ inputs.type }} path: /tmp/build @@ -71,26 +75,64 @@ runs: tar xzf Rocket.Chat.tar.gz rm Rocket.Chat.tar.gz - - name: Build Docker images - shell: bash - run: | - export DENO_VERSION="${{ inputs.deno-version }}" + - name: Set up Docker + uses: docker/setup-docker-action@v4 + with: + daemon-config: | + { + "debug": true, + "features": { + "containerd-snapshotter": true + } + } - docker compose -f docker-compose-ci.yml build ${{ inputs.service }} + - uses: docker/setup-buildx-action@v3 + with: + buildkitd-flags: --oci-worker-gc --oci-worker-gc-keepstorage=4000 - - name: Publish Docker images to GitHub Container Registry - if: inputs.publish-image == 'true' && github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') + - name: Build Docker images shell: bash run: | set -o xtrace + export DENO_VERSION="${{ inputs.deno-version }}" + + if [[ "${{ inputs.publish-image }}" == 'true' ]]; then + LOAD_OR_PUSH="--push" + else + LOAD_OR_PUSH="--load" + fi # Get image name from docker-compose-ci.yml since rocketchat image is different from service name (rocket.chat) IMAGE=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "${{ inputs.service }}" '.services[$s].image') - IMAGE_NO_TAG=$(echo "$IMAGE" | sed 's/:.*$//') - docker tag ${IMAGE} ${IMAGE}-gha-run-${{ github.run_id }} + docker buildx bake \ + -f docker-compose-ci.yml \ + ${LOAD_OR_PUSH} \ + --allow=fs.read=/tmp/build \ + --set "*.tags+=${IMAGE}-gha-run-${{ github.run_id }}" \ + --set "*.labels.org.opencontainers.image.description=Build run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ + --set *.platform=linux/${{ inputs.arch }} \ + --set *.cache-from=type=gha \ + --set *.cache-to=type=gha,mode=max \ + --provenance=false \ + --sbom=false \ + --metadata-file "/tmp/meta.json" \ + "${{ inputs.service }}" - docker push --all-tags ${IMAGE_NO_TAG} + echo "Contents of /tmp/meta.json:" + cat /tmp/meta.json + + mkdir -p /tmp/digests/${{ inputs.service }}${{ inputs.type == 'coverage' && '-cov' || '' }}/${{ inputs.arch }} + DIGEST=$(jq -r '.["${{ inputs.service }}"].["containerimage.digest"]' "/tmp/meta.json") + IMAGE_NO_TAG=$(echo "$IMAGE" | sed 's/:.*$//') + echo "${IMAGE_NO_TAG}@${DIGEST}" > "/tmp/digests/${{ inputs.service }}${{ inputs.type == 'coverage' && '-cov' || '' }}/${{ inputs.arch }}/digest.txt" + + - uses: actions/upload-artifact@v4 + if: inputs.publish-image == 'true' + with: + name: digests-${{ inputs.service }}-${{ inputs.arch }}-${{ inputs.type }} + path: /tmp/digests + retention-days: 5 - name: Clean up temporary files if: inputs.service == 'rocketchat' diff --git a/.github/actions/meteor-build/action.yml b/.github/actions/meteor-build/action.yml index daa06dd8d1f70..2c6d965bca462 100644 --- a/.github/actions/meteor-build/action.yml +++ b/.github/actions/meteor-build/action.yml @@ -129,7 +129,7 @@ runs: run: meteor reset - name: Restore packages build - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v6 with: name: packages-build path: /tmp diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index 6974fb8c9c858..7d1e253bfd288 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -46,7 +46,7 @@ runs: apps/meteor/ee/server/services/node_modules packages/apps-engine/node_modules packages/apps-engine/.deno-cache - key: node-modules-${{ hashFiles('yarn.lock') }}-deno-v${{ inputs.deno-version }}-${{ hashFiles('packages/apps-engine/deno-runtime/deno.lock') }} + key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('yarn.lock') }}-deno-v${{ inputs.deno-version }}-${{ hashFiles('packages/apps-engine/deno-runtime/deno.lock') }} # # Could use this command to list all paths to save: # find . -name 'node_modules' -prune | grep -v "/\.meteor/" | grep -v "/meteor/packages/" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a86ab4298a8f..4e142116e7b31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -267,21 +267,23 @@ jobs: build-gh-docker: name: 🚢 Build Docker needs: [build, release-versions] - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04${{ matrix.arch == 'arm64' && '-arm' || '' }} env: - DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} + DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }}-${{ matrix.arch }} LOWERCASE_REPOSITORY: ${{ needs.release-versions.outputs.lowercase-repo }} strategy: fail-fast: false matrix: + arch: ['amd64', 'arm64'] service: ${{ fromJson(needs.release-versions.outputs.services) }} type: ['production'] include: # for rocketchat monolith and on develop branch builds we create a coverage image to run tests against it # and keep the baseline of the coverage for other PRs - - service: rocketchat + - arch: amd64 + service: rocketchat type: coverage steps: @@ -298,9 +300,79 @@ jobs: node-version: ${{ needs.release-versions.outputs.node-version }} deno-version: ${{ needs.release-versions.outputs.deno-version }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + arch: ${{ matrix.arch }} service: ${{ matrix.service }} type: ${{ matrix.type }} + build-gh-docker-publish: + name: 🚢 Publish Docker Images (ghcr.io) + needs: [build-gh-docker, release-versions] + runs-on: ubuntu-24.04 + + env: + DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} + LOWERCASE_REPOSITORY: ${{ needs.release-versions.outputs.lowercase-repo }} + + steps: + - uses: actions/checkout@v4 + if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') + with: + sparse-checkout: | + docker-compose-ci.yml + sparse-checkout-cone-mode: false + ref: ${{ github.ref }} + + - name: Login to GitHub Container Registry + if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ secrets.CR_USER }} + password: ${{ secrets.CR_PAT }} + + - name: Download digests + if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') + uses: actions/download-artifact@v6 + with: + pattern: digests-* + path: /tmp/digests + merge-multiple: true + + - name: Create and push multi-arch manifests + if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') + run: | + set -o xtrace + shopt -s nullglob + + for service_dir in /tmp/digests/*; do + [[ -d "$service_dir" ]] || continue + service="$(basename "$service_dir")" + echo "Creating manifest for $service" + + mapfile -t refs < <( + find "$service_dir" -type f -name 'digest.txt' -print0 \ + | xargs -0 -I{} sh -c "tr -d '\r' < '{}' | sed '/^[[:space:]]*$/d'" + ) + + echo "Digest for ${service}: ${refs[@]}" + + # Get image name from docker-compose-ci.yml since rocketchat image is different from service name (rocket.chat) + if [ "$service" == "rocketchat-cov" ]; then + IMAGE=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "rocketchat" '.services[$s].image')-cov + else + IMAGE=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "$service" '.services[$s].image') + fi + + echo $IMAGE + + docker buildx imagetools create \ + --debug \ + --annotation "manifest-descriptor:org.opencontainers.image.description=Build run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ + --tag "${IMAGE}" \ + --tag "${IMAGE}-gha-run-${{ github.run_id }}" \ + ${refs[@]} + done + checks: needs: [release-versions, packages-build] @@ -335,7 +407,7 @@ jobs: test-api: name: 🔨 Test API (CE) - needs: [checks, build-gh-docker, release-versions] + needs: [checks, build-gh-docker-publish, release-versions] uses: ./.github/workflows/ci-test-e2e.yml with: @@ -351,7 +423,7 @@ jobs: test-ui: name: 🔨 Test UI (CE) - needs: [checks, build-gh-docker, release-versions] + needs: [checks, build-gh-docker-publish, release-versions] uses: ./.github/workflows/ci-test-e2e.yml with: @@ -376,7 +448,7 @@ jobs: test-api-ee: name: 🔨 Test API (EE) - needs: [checks, build-gh-docker, release-versions] + needs: [checks, build-gh-docker-publish, release-versions] uses: ./.github/workflows/ci-test-e2e.yml with: @@ -395,7 +467,7 @@ jobs: test-ui-ee: name: 🔨 Test UI (EE) - needs: [checks, build-gh-docker, release-versions] + needs: [checks, build-gh-docker-publish, release-versions] uses: ./.github/workflows/ci-test-e2e.yml with: @@ -422,7 +494,7 @@ jobs: test-ui-ee-watcher: name: 🔨 Test UI (EE) - needs: [checks, build-gh-docker, release-versions] + needs: [checks, build-gh-docker-publish, release-versions] uses: ./.github/workflows/ci-test-e2e.yml with: @@ -490,7 +562,7 @@ jobs: name: 🚀 Publish build assets runs-on: ubuntu-24.04 if: github.event_name == 'release' || github.ref == 'refs/heads/develop' - needs: [build-gh-docker, release-versions] + needs: [build-gh-docker-publish, release-versions] steps: - uses: actions/checkout@v5 @@ -587,8 +659,13 @@ jobs: - name: Pull Docker image run: docker pull ${{ steps.gh-docker.outputs.gh-image-name }} - - name: Publish Docker image + - name: Publish Docker images run: | + set -euo pipefail + + sudo apt-get update -y + sudo apt-get install -y skopeo + if [[ '${{ matrix.service }}' == 'rocketchat' ]]; then IMAGE_NAME="${{ needs.release-versions.outputs.lowercase-repo }}/rocket.chat" else @@ -624,17 +701,25 @@ jobs: echo "Tags: ${TAGS[*]}" + # get first tag as primary + PRIMARY="${TAGS[0]}" + SRC="${{ steps.gh-docker.outputs.gh-image-name }}" DEST_REPO="docker.io/${IMAGE_NAME}" - if (( ${#TAGS[@]} > 0 )); then - for t in "${TAGS[@]:0}"; do - echo "Copying $SRC to ${DEST_REPO}:${t}" - docker tag $SRC "${DEST_REPO}:${t}" + # build --additional-tag for all other tags + EXTRA_ARGS=() + if (( ${#TAGS[@]} > 1 )); then + for t in "${TAGS[@]:1}"; do + EXTRA_ARGS+=( --additional-tag "${DEST_REPO}:${t}" ) done fi - docker push --all-tags $DEST_REPO + echo "Copying $SRC to ${DEST_REPO}:${PRIMARY} with extras ${EXTRA_ARGS[*]}" + skopeo copy --all \ + "docker://${SRC}" \ + "docker://${DEST_REPO}:${PRIMARY}" \ + "${EXTRA_ARGS[@]}" notify-services: name: 🚀 Notify external services diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index 0f7f2d41e8d41..75ae6f44672e1 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -6,6 +6,10 @@ services: build: dockerfile: ${GITHUB_WORKSPACE}/apps/meteor/.docker/Dockerfile.alpine context: /tmp/build + x-bake: + platforms: + - linux/amd64 + - linux/arm64 args: DENO_VERSION: ${DENO_VERSION} image: ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${DOCKER_TAG}${DOCKER_TAG_SUFFIX_ROCKETCHAT} @@ -40,6 +44,10 @@ services: build: dockerfile: ee/apps/authorization-service/Dockerfile context: . + x-bake: + platforms: + - linux/amd64 + - linux/arm64 args: SERVICE: authorization-service DENO_VERSION: ${DENO_VERSION} @@ -58,6 +66,10 @@ services: build: dockerfile: ee/apps/account-service/Dockerfile context: . + x-bake: + platforms: + - linux/amd64 + - linux/arm64 args: SERVICE: account-service image: ghcr.io/${LOWERCASE_REPOSITORY}/account-service:${DOCKER_TAG} @@ -75,6 +87,10 @@ services: build: dockerfile: ee/apps/presence-service/Dockerfile context: . + x-bake: + platforms: + - linux/amd64 + - linux/arm64 args: SERVICE: presence-service DENO_VERSION: ${DENO_VERSION} @@ -93,6 +109,10 @@ services: build: dockerfile: ee/apps/ddp-streamer/Dockerfile context: . + x-bake: + platforms: + - linux/amd64 + - linux/arm64 args: SERVICE: ddp-streamer image: ghcr.io/${LOWERCASE_REPOSITORY}/ddp-streamer-service:${DOCKER_TAG} @@ -116,6 +136,10 @@ services: build: dockerfile: ee/apps/stream-hub-service/Dockerfile context: . + x-bake: + platforms: + - linux/amd64 + - linux/arm64 args: SERVICE: stream-hub-service image: ghcr.io/${LOWERCASE_REPOSITORY}/stream-hub-service:${DOCKER_TAG} @@ -134,6 +158,10 @@ services: build: dockerfile: ee/apps/queue-worker/Dockerfile context: . + x-bake: + platforms: + - linux/amd64 + - linux/arm64 args: SERVICE: queue-worker image: ghcr.io/${LOWERCASE_REPOSITORY}/queue-worker-service:${DOCKER_TAG} @@ -151,6 +179,10 @@ services: build: dockerfile: ee/apps/omnichannel-transcript/Dockerfile context: . + x-bake: + platforms: + - linux/amd64 + - linux/arm64 args: SERVICE: omnichannel-transcript image: ghcr.io/${LOWERCASE_REPOSITORY}/omnichannel-transcript-service:${DOCKER_TAG}