From 9c148aed17367361474cf20e6f9de96d4bdf3aea Mon Sep 17 00:00:00 2001 From: dsx137 <70027572+dsx137@users.noreply.github.com> Date: Mon, 6 Jul 2026 10:37:53 +0800 Subject: [PATCH 1/3] ci: serialize release image workflows --- .github/workflows/build-docker-image.yml | 31 +++- ...cker-build-and-release-services-images.yml | 27 +++- .github/workflows/release.yml | 139 ++++++------------ .../update-version-file-on-release.yml | 69 ++++++++- 4 files changed, 159 insertions(+), 107 deletions(-) diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index d7578920..023a553d 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -7,22 +7,38 @@ on: - release-* # tags: # - v* - release: - types: [published] - workflow_dispatch: inputs: tag: description: "Tag version to be used for Docker image" required: true default: "v3.8.0" + workflow_call: + inputs: + tag: + description: "Tag version to be used for Docker image" + required: true + type: string + checkout_sha: + description: "Commit SHA to checkout" + required: true + type: string + checkout_short_sha: + description: "Short commit SHA for image tags" + required: true + type: string jobs: build-image: runs-on: ubuntu-latest + permissions: + contents: read + packages: write steps: - name: Checkout uses: actions/checkout@v7 + with: + ref: ${{ inputs.checkout_sha || inputs.tag || github.event.inputs.tag }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 @@ -63,8 +79,10 @@ jobs: # type=semver,pattern={{version}} type=semver,pattern=v{{version}} type=semver,pattern=release-{{raw}} - type=sha - type=raw,value=${{ github.event.inputs.tag }} + type=sha,enable=${{ inputs.checkout_short_sha == '' }} + type=raw,value=sha-${{ inputs.checkout_short_sha }},enable=${{ inputs.checkout_short_sha != '' }} + type=raw,value=${{ inputs.tag || github.event.inputs.tag }} + type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} - name: Build and push Docker image uses: docker/build-push-action@v7 @@ -79,7 +97,8 @@ jobs: run: | images=("openim/openim-chat" "ghcr.io/openimsdk/openim-chat" "registry.cn-hangzhou.aliyuncs.com/openimsdk/openim-chat") for image in "${images[@]}"; do - for tag in $(echo "${{ steps.meta.outputs.tags }}" | tr ',' '\n'); do + for ref in $(echo "${{ steps.meta.outputs.tags }}" | tr ',' '\n'); do + tag="${ref##*:}" manifest=$(docker manifest inspect "$image:$tag" || echo "error") if [[ "$manifest" == "error" ]]; then echo "Manifest not found for $image:$tag" diff --git a/.github/workflows/docker-build-and-release-services-images.yml b/.github/workflows/docker-build-and-release-services-images.yml index e6e4f46d..ffd4db44 100644 --- a/.github/workflows/docker-build-and-release-services-images.yml +++ b/.github/workflows/docker-build-and-release-services-images.yml @@ -4,22 +4,39 @@ on: push: branches: - release-* - release: - types: [published] workflow_dispatch: inputs: tag: description: "Tag version to be used for Docker image" required: true default: "v3.8.3" + workflow_call: + inputs: + tag: + description: "Tag version to be used for Docker image" + required: true + type: string + checkout_sha: + description: "Commit SHA to checkout" + required: true + type: string + checkout_short_sha: + description: "Short commit SHA for image tags" + required: true + type: string jobs: build-and-push: runs-on: ubuntu-latest + permissions: + contents: read + packages: write steps: - name: Checkout repository uses: actions/checkout@v7 + with: + ref: ${{ inputs.checkout_sha || inputs.tag || github.event.inputs.tag }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 @@ -54,8 +71,10 @@ jobs: type=ref,event=branch type=semver,pattern=v{{version}} type=semver,pattern=release-{{raw}} - type=sha - type=raw,value=${{ github.event.inputs.tag }} + type=sha,enable=${{ inputs.checkout_short_sha == '' }} + type=raw,value=sha-${{ inputs.checkout_short_sha }},enable=${{ inputs.checkout_short_sha != '' }} + type=raw,value=${{ inputs.tag || github.event.inputs.tag }} + type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} - name: Build and push Docker images env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b2204f39..468a709a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,99 +1,56 @@ -name: OpenIM chat release +name: Release on: - push: - # run only against tags - tags: - - '*' + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: "Existing tag to release" + required: true + target_branch: + description: "Branch that the release tag was created from" + required: true permissions: + actions: write contents: write packages: write - issues: write -jobs: - goreleaser: - runs-on: ubuntu-latest - env: - DOCKER_CLI_EXPERIMENTAL: "enabled" - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: setup-snapcraft - # FIXME: the mkdirs are a hack for https://github.com/goreleaser/goreleaser/issues/1715 - run: | - sudo apt-get update - sudo apt-get -yq --no-install-suggests --no-install-recommends install snapcraft - mkdir -p $HOME/.cache/snapcraft/download - mkdir -p $HOME/.cache/snapcraft/stage-packages - - - uses: actions/setup-go@v6 - with: - go-version: stable - - - name: Set up QEMU - uses: docker/setup-qemu-action@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - - name: Login to Docker Hub - uses: docker/login-action@v4 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} +concurrency: + group: release-${{ github.event.release.tag_name || inputs.tag }} + cancel-in-progress: false - - name: Login to GitHub Container Registry - uses: docker/login-action@v4 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Log in to AliYun Docker Hub - uses: docker/login-action@v4 - with: - registry: registry.cn-hangzhou.aliyuncs.com - username: ${{ secrets.ALIREGISTRY_USERNAME }} - password: ${{ secrets.ALIREGISTRY_TOKEN }} - - - name: set action env cache - uses: actions/cache@v4 - with: - path: | - ./_output/dist/*.deb - ./_output/dist/*.rpm - ./_output/dist/*.apk - key: ${{ github.ref }} - - - uses: sigstore/cosign-installer@v4 - - uses: anchore/sbom-action/download-syft@v0.24.0 - - uses: crazy-max/ghaction-upx@v4 - with: - install-only: true - - uses: cachix/install-nix-action@v31 - with: - github_access_token: ${{ secrets.GITHUB_TOKEN }} - # - name: snapcraft-login - # if: startsWith(github.ref, 'refs/tags/v') - # run: snapcraft login --with <(echo "${{ secrets.SNAPCRAFT_TOKEN }}") - # More assembly might be required: Docker logins, GPG, etc. It all depends - # on your needs. - - - uses: goreleaser/goreleaser-action@v7 - with: - # either 'goreleaser' (default) or 'goreleaser-pro': - distribution: goreleaser - version: latest - args: release --clean - env: - GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} - USERNAME: ${{ github.repository_owner }} - # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' - # distribution: - # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} - FURY_TOKEN: ${{ secrets.FURY_TOKEN }} - +jobs: + update-version: + permissions: + contents: write + uses: ./.github/workflows/update-version-file-on-release.yml + with: + tag: ${{ github.event.release.tag_name || inputs.tag }} + target_branch: ${{ github.event.release.target_commitish || inputs.target_branch }} + secrets: inherit + + docker-image: + needs: update-version + permissions: + contents: read + packages: write + uses: ./.github/workflows/build-docker-image.yml + with: + tag: ${{ github.event.release.tag_name || inputs.tag }} + checkout_sha: ${{ needs.update-version.outputs.updated_sha }} + checkout_short_sha: ${{ needs.update-version.outputs.updated_short_sha }} + secrets: inherit + + service-images: + needs: update-version + permissions: + contents: read + packages: write + uses: ./.github/workflows/docker-build-and-release-services-images.yml + with: + tag: ${{ github.event.release.tag_name || inputs.tag }} + checkout_sha: ${{ needs.update-version.outputs.updated_sha }} + checkout_short_sha: ${{ needs.update-version.outputs.updated_short_sha }} + secrets: inherit diff --git a/.github/workflows/update-version-file-on-release.yml b/.github/workflows/update-version-file-on-release.yml index 1222dd3b..d38110c8 100644 --- a/.github/workflows/update-version-file-on-release.yml +++ b/.github/workflows/update-version-file-on-release.yml @@ -1,22 +1,66 @@ name: Update Version File on Release on: - release: - types: [created] + workflow_dispatch: + inputs: + tag: + description: "Tag version to update" + required: true + target_branch: + description: "Branch that the release tag was created from" + required: true + workflow_call: + inputs: + tag: + description: "Tag version to update" + required: true + type: string + target_branch: + description: "Branch that the release tag was created from" + required: true + type: string + outputs: + updated_sha: + description: "Commit SHA after updating version and moving the tag" + value: ${{ jobs.update-version.outputs.updated_sha }} + updated_short_sha: + description: "Short commit SHA after updating version and moving the tag" + value: ${{ jobs.update-version.outputs.updated_short_sha }} + +permissions: + contents: write jobs: update-version: runs-on: ubuntu-latest + outputs: + updated_sha: ${{ steps.resolve-tag.outputs.updated_sha }} + updated_short_sha: ${{ steps.resolve-tag.outputs.updated_short_sha }} env: - TAG_VERSION: ${{ github.event.release.tag_name }} + TAG_VERSION: ${{ inputs.tag || github.event.inputs.tag }} + TARGET_BRANCH: ${{ inputs.target_branch || github.event.inputs.target_branch }} steps: # Step 1: Checkout the original repository's code - name: Checkout code uses: actions/checkout@v7 with: + ref: ${{ inputs.target_branch || github.event.inputs.target_branch }} fetch-depth: 0 # submodules: "recursive" + - name: Validate target branch + run: | + set -euo pipefail + if [[ -z "${TARGET_BRANCH}" ]]; then + echo "target_branch is required" + exit 1 + fi + if ! git ls-remote --exit-code --heads origin "${TARGET_BRANCH}" >/dev/null; then + echo "target_branch must be an existing branch: ${TARGET_BRANCH}" + exit 1 + fi + git checkout -B "${TARGET_BRANCH}" "origin/${TARGET_BRANCH}" + - name: Safe submodule initialization run: | echo "Checking for submodules..." @@ -48,7 +92,6 @@ jobs: run: | if git rev-parse ${{ env.TAG_VERSION }} >/dev/null 2>&1; then git tag -d ${{ env.TAG_VERSION }} - git push --delete origin ${{ env.TAG_VERSION }} fi # Step 4: Update version file @@ -62,14 +105,28 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + set -euo pipefail git add version/version - git commit -m "Update version to ${{ env.TAG_VERSION }}" + if git diff --cached --quiet; then + echo "version/version already matches ${TAG_VERSION}" + else + git commit -m "Update version to ${{ env.TAG_VERSION }}" + git push origin HEAD:${TARGET_BRANCH} + fi # Step 6: Update tag - name: Update tag run: | + set -euo pipefail git tag -fa ${{ env.TAG_VERSION }} -m "Update version to ${{ env.TAG_VERSION }}" - git push origin ${{ env.TAG_VERSION }} --force + git push origin refs/tags/${{ env.TAG_VERSION }}:refs/tags/${{ env.TAG_VERSION }} --force + + - name: Resolve updated tag SHA + id: resolve-tag + run: | + updated_sha="$(git rev-parse "${TAG_VERSION}^{commit}")" + echo "updated_sha=$updated_sha" >> "$GITHUB_OUTPUT" + echo "updated_short_sha=${updated_sha:0:12}" >> "$GITHUB_OUTPUT" # Step 7: Find and Publish Draft Release - name: Find and Publish Draft Release From a9ae4933b91e3dfbda1a1fc98408dcc6c76b8bea Mon Sep 17 00:00:00 2001 From: dsx137 <70027572+dsx137@users.noreply.github.com> Date: Mon, 6 Jul 2026 18:19:01 +0800 Subject: [PATCH 2/3] feat(core): split Docker build into per-service jobs Add build/build_single.sh and update the workflow to resolve the service matrix, then build and push each service separately. --- ...cker-build-and-release-services-images.yml | 25 +++- build/build_single.sh | 127 ++++++++++++++++++ 2 files changed, 151 insertions(+), 1 deletion(-) create mode 100755 build/build_single.sh diff --git a/.github/workflows/docker-build-and-release-services-images.yml b/.github/workflows/docker-build-and-release-services-images.yml index ffd4db44..7d438971 100644 --- a/.github/workflows/docker-build-and-release-services-images.yml +++ b/.github/workflows/docker-build-and-release-services-images.yml @@ -26,8 +26,30 @@ on: type: string jobs: + services: + runs-on: ubuntu-latest + outputs: + services: ${{ steps.services.outputs.services }} + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + ref: ${{ inputs.checkout_sha || inputs.tag || github.event.inputs.tag }} + + - name: Resolve service matrix + id: services + run: | + set -euo pipefail + services=$(docker compose -f build/images/openim-chat/docker-compose.build.yml config --services | jq -R -s -c 'split("\n") | map(select(length > 0))') + echo "services=$services" >> "$GITHUB_OUTPUT" + build-and-push: + needs: services runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + service: ${{ fromJson(needs.services.outputs.services) }} permissions: contents: read packages: write @@ -80,9 +102,10 @@ jobs: env: PUSH: "true" RELEASE: "true" + SERVICE: ${{ matrix.service }} IMAGE_TAGS: ${{ steps.meta.outputs.tags }} IMAGE_REGISTRIES: | ${{ secrets.DOCKER_USERNAME }} ghcr.io/${{ github.repository_owner }} registry.cn-hangzhou.aliyuncs.com/openimsdk - run: build/build.sh + run: build/build_single.sh diff --git a/build/build_single.sh b/build/build_single.sh new file mode 100755 index 00000000..a85d5581 --- /dev/null +++ b/build/build_single.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash + +set -euo pipefail + +CYAN='\033[0;36m' +GREEN='\033[0;32m' +RED='\033[0;31m' +NO_COLOR='\033[0m' + +BASE_DIR="$(cd "$(dirname "$0")" && pwd)" +COMPOSE_FILE="$BASE_DIR/images/openim-chat/docker-compose.build.yml" +RELEASE="${RELEASE:-false}" +PUSH="${PUSH:-false}" +DRY_RUN="${DRY_RUN:-false}" +PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}" +IMAGE_TAGS="${IMAGE_TAGS:-}" +IMAGE_REGISTRIES="${IMAGE_REGISTRIES:-}" +SERVICE="${SERVICE:-}" + +if [[ ! -f "$COMPOSE_FILE" ]]; then + echo -e "${RED}docker-compose.build.yml not found: $COMPOSE_FILE${NO_COLOR}" + exit 1 +fi + +if [[ -z "$SERVICE" ]]; then + echo -e "${RED}SERVICE is required${NO_COLOR}" + exit 1 +fi + +cd "$BASE_DIR/.." || exit 1 + +split_values() { + echo "$1" | grep -o '[^, ]\+' +} + +run_or_print() { + if [[ "$DRY_RUN" == "true" ]]; then + printf '%q ' "$@" + printf '\n' + else + "$@" + fi +} + +build_local() { + echo -e "${CYAN}Building $SERVICE...${NO_COLOR}" + RELEASE="$RELEASE" docker compose -f "$COMPOSE_FILE" build "$SERVICE" + + built_image=$(docker compose -f "$COMPOSE_FILE" config --images "$SERVICE" 2>/dev/null || true) + if [[ -n "$built_image" ]]; then + compatibility_tag="${built_image##*/}" + if [[ "$built_image" != "$compatibility_tag" ]]; then + docker tag "$built_image" "$compatibility_tag" + fi + fi + + echo -e "${GREEN}Successfully built $SERVICE${NO_COLOR}" +} + +build_push() { + if ! command -v jq >/dev/null 2>&1; then + echo -e "${RED}jq is required for PUSH=true${NO_COLOR}" + exit 1 + fi + + if [[ -z "$IMAGE_TAGS" || -z "$IMAGE_REGISTRIES" ]]; then + echo -e "${RED}IMAGE_TAGS and IMAGE_REGISTRIES are required for PUSH=true${NO_COLOR}" + exit 1 + fi + + image_tags=() + while IFS= read -r tag; do + image_tags+=("$tag") + done < <(split_values "$IMAGE_TAGS") + + image_registries=() + while IFS= read -r registry; do + image_registries+=("$registry") + done < <(split_values "$IMAGE_REGISTRIES") + + if [[ ${#image_tags[@]} -eq 0 || ${#image_registries[@]} -eq 0 ]]; then + echo -e "${RED}IMAGE_TAGS and IMAGE_REGISTRIES must contain at least one value${NO_COLOR}" + exit 1 + fi + + compose_config=$(docker compose -f "$COMPOSE_FILE" config --format json) + context=$(jq -r --arg service "$SERVICE" '.services[$service].build.context // empty' <<< "$compose_config") + dockerfile=$(jq -r --arg service "$SERVICE" '.services[$service].build.dockerfile // empty' <<< "$compose_config") + cmd_path=$(jq -r --arg service "$SERVICE" '.services[$service].build.args.CMD_PATH // empty' <<< "$compose_config") + binary_name=$(jq -r --arg service "$SERVICE" '.services[$service].build.args.BINARY_NAME // empty' <<< "$compose_config") + + if [[ -z "$context" || -z "$dockerfile" || -z "$cmd_path" || -z "$binary_name" ]]; then + echo -e "${RED}Invalid build config for $SERVICE${NO_COLOR}" + exit 1 + fi + + if [[ ! -d "$cmd_path" && ! -f "$cmd_path/main.go" ]]; then + echo -e "${CYAN}Skipping $SERVICE because $cmd_path does not exist${NO_COLOR}" + exit 0 + fi + + tag_args=() + for registry in "${image_registries[@]}"; do + for tag in "${image_tags[@]}"; do + tag_args+=(--tag "$registry/$binary_name:$tag") + done + done + + echo -e "${CYAN}Building $binary_name for $PLATFORMS...${NO_COLOR}" + run_or_print docker buildx build \ + --platform "$PLATFORMS" \ + --file "$dockerfile" \ + --build-arg "CMD_PATH=$cmd_path" \ + --build-arg "BINARY_NAME=$binary_name" \ + --build-arg "RELEASE=$RELEASE" \ + "${tag_args[@]}" \ + --push \ + "$context" + + echo -e "${GREEN}Successfully pushed $SERVICE${NO_COLOR}" +} + +if [[ "$PUSH" == "true" ]]; then + build_push +else + build_local +fi From 7eed50aa6e74940316806efc76c0c4822a1e84f1 Mon Sep 17 00:00:00 2001 From: dsx137 <70027572+dsx137@users.noreply.github.com> Date: Mon, 6 Jul 2026 18:37:07 +0800 Subject: [PATCH 3/3] chore(ci): remove draft release publishing step --- .../update-version-file-on-release.yml | 46 ------------------- 1 file changed, 46 deletions(-) diff --git a/.github/workflows/update-version-file-on-release.yml b/.github/workflows/update-version-file-on-release.yml index d38110c8..d4fa345d 100644 --- a/.github/workflows/update-version-file-on-release.yml +++ b/.github/workflows/update-version-file-on-release.yml @@ -128,49 +128,3 @@ jobs: echo "updated_sha=$updated_sha" >> "$GITHUB_OUTPUT" echo "updated_short_sha=${updated_sha:0:12}" >> "$GITHUB_OUTPUT" - # Step 7: Find and Publish Draft Release - - name: Find and Publish Draft Release - uses: actions/github-script@v9 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { owner, repo } = context.repo; - const tagName = process.env.TAG_VERSION; - - try { - let release; - try { - const response = await github.rest.repos.getReleaseByTag({ - owner, - repo, - tag: tagName - }); - release = response.data; - } catch (tagError) { - core.info(`Release not found by tag, searching all releases...`); - const releases = await github.rest.repos.listReleases({ - owner, - repo, - per_page: 100 - }); - - release = releases.data.find(r => r.draft && r.tag_name === tagName); - if (!release) { - throw new Error(`No release found with tag ${tagName}`); - } - } - - await github.rest.repos.updateRelease({ - owner, - repo, - release_id: release.id, - draft: false, - prerelease: release.prerelease - }); - - const status = release.draft ? "was draft" : "was already published"; - core.info(`Release ${tagName} ensured to be published (${status}).`); - - } catch (error) { - core.warning(`Could not find or update release for tag ${tagName}: ${error.message}`); - }