From de0f1eec8a2dd4808dccff01d68685b13006686d Mon Sep 17 00:00:00 2001 From: dsx137 <70027572+dsx137@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:27:14 +0800 Subject: [PATCH] ci: action matrix release (#3761) * ci: serialize release image workflows * feat(build): add single-service Docker build script * chore(ci): remove draft release publishing step # Conflicts: # .github/workflows/update-version-file-on-release.yml --- ...cker-build-and-release-services-images.yml | 52 +++++++- .github/workflows/publish-docker-image.yml | 25 +++- .github/workflows/release.yml | 56 +++++++++ .../update-version-file-on-release.yml | 113 ++++++++++-------- build/build_single.sh | 105 ++++++++++++++++ 5 files changed, 292 insertions(+), 59 deletions(-) create mode 100644 .github/workflows/release.yml 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 941f83cfa..27b22be31 100644 --- a/.github/workflows/docker-build-and-release-services-images.yml +++ b/.github/workflows/docker-build-and-release-services-images.yml @@ -4,18 +4,52 @@ 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: + services: + runs-on: ubuntu-latest + outputs: + services: ${{ steps.services.outputs.services }} + steps: + - name: Checkout repository + uses: actions/checkout@v7.0.0 + 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-server/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 @@ -23,6 +57,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v7.0.0 + with: + ref: ${{ inputs.checkout_sha || inputs.tag || github.event.inputs.tag }} - name: Set up QEMU uses: docker/setup-qemu-action@v4.1.0 @@ -53,8 +89,9 @@ jobs: - name: Resolve Docker image tags id: tags env: - INPUT_TAG: ${{ github.event.inputs.tag }} + INPUT_TAG: ${{ inputs.tag || github.event.inputs.tag }} RELEASE_TAG: ${{ github.event.release.tag_name }} + CHECKOUT_SHORT_SHA: ${{ inputs.checkout_short_sha }} run: | set -euo pipefail @@ -84,13 +121,17 @@ jobs: add_tag_with_semver_alias "${INPUT_TAG:-}" add_tag_with_semver_alias "${RELEASE_TAG:-}" + if [[ "${GITHUB_EVENT_NAME:-}" == "release" && "${{ github.event.release.prerelease }}" != "true" ]]; then + add_tag "latest" + fi + if [[ "${GITHUB_REF_TYPE:-}" == "tag" ]]; then add_tag_with_semver_alias "${GITHUB_REF_NAME}" elif [[ "${GITHUB_REF_TYPE:-}" == "branch" ]]; then add_tag "${GITHUB_REF_NAME}" fi - add_tag "sha-${GITHUB_SHA::7}" + add_tag "sha-${CHECKOUT_SHORT_SHA:-${GITHUB_SHA::7}}" { echo "tags</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,58 +105,26 @@ 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 - # Step 7: Find and Publish Draft Release - - name: Find and Publish Draft Release - uses: actions/github-script@v9.0.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { owner, repo } = context.repo; - const tagName = process.env.TAG_VERSION; + - 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" - 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}`); - } diff --git a/build/build_single.sh b/build/build_single.sh new file mode 100755 index 000000000..f5b1e8075 --- /dev/null +++ b/build/build_single.sh @@ -0,0 +1,105 @@ +#!/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-server/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 +} + +if ! command -v jq >/dev/null 2>&1; then + echo -e "${RED}jq is required${NO_COLOR}" + exit 1 +fi + +if [[ -z "$IMAGE_TAGS" || -z "$IMAGE_REGISTRIES" ]]; then + echo -e "${RED}IMAGE_TAGS and IMAGE_REGISTRIES are required${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 $binary_name${NO_COLOR}"