name: Harbor Build Once → Deploy Many (Compose) on: workflow_call: inputs: harbor_registry: type: string default: harbor.hcmc.online harbor_project: type: string default: ci image_repo: type: string required: true context: type: string default: . dockerfile: type: string default: Dockerfile platforms: type: string default: linux/amd64,linux/arm64 # IMPORTANT CHANGE deploy_runners_json: description: 'JSON array of runner labels' type: string required: true compose_workdir: type: string default: . compose_args: type: string default: up -d --pull always --remove-orphans secrets: HARBOR_PUSH_USERNAME: { required: true } HARBOR_PUSH_PASSWORD: { required: true } HARBOR_PULL_USERNAME: { required: true } HARBOR_PULL_PASSWORD: { required: true } jobs: # ========================================================== # BUILD ONCE (single powerful runner) # ========================================================== build_and_push: name: Build & Push Image runs-on: devsg-atlantic outputs: image_repo: ${{ steps.meta.outputs.image_repo }} image_digest: ${{ steps.build.outputs.digest }} image_ref: ${{ steps.meta.outputs.image_repo }}@${{ steps.build.outputs.digest }} steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 - name: Compute image metadata id: meta shell: bash run: | set -euo pipefail IMAGE="${{ inputs.harbor_registry }}/${{ inputs.harbor_project }}/${{ inputs.image_repo }}" SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-12) BRANCH="${{ github.ref_name }}" echo "image_repo=$IMAGE" >> $GITHUB_OUTPUT echo "sha_tag=sha-$SHORT_SHA" >> $GITHUB_OUTPUT echo "branch_tag=$BRANCH" >> $GITHUB_OUTPUT - name: Login Harbor (push) uses: docker/login-action@v3 with: registry: ${{ inputs.harbor_registry }} username: ${{ secrets.HARBOR_PUSH_USERNAME }} password: ${{ secrets.HARBOR_PUSH_PASSWORD }} # Cached multi-platform build - name: Build & Push Image id: build uses: docker/build-push-action@v6 with: context: ${{ inputs.context }} file: ${{ inputs.dockerfile }} platforms: ${{ inputs.platforms }} push: true tags: | ${{ steps.meta.outputs.image_repo }}:${{ steps.meta.outputs.sha_tag }} ${{ steps.meta.outputs.image_repo }}:${{ steps.meta.outputs.branch_tag }} ${{ steps.meta.outputs.image_repo }}:latest cache-from: type=registry,ref=${{ steps.meta.outputs.image_repo }}:buildcache cache-to: type=registry,ref=${{ steps.meta.outputs.image_repo }}:buildcache,mode=max # ========================================================== # DEPLOY MANY (fan-out runners) # ========================================================== deploy: name: Deploy to Fleet needs: build_and_push strategy: fail-fast: false matrix: runner: ${{ fromJSON(inputs.deploy_runners_json) }} runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v4 - name: Login Harbor (pull) uses: docker/login-action@v3 with: registry: ${{ inputs.harbor_registry }} username: ${{ secrets.HARBOR_PULL_USERNAME }} password: ${{ secrets.HARBOR_PULL_PASSWORD }} - name: Show image run: echo "Deploying ${{ needs.build_and_push.outputs.image_ref }}" # Linux/macOS - name: Compose Up (Unix) if: runner.os != 'Windows' shell: bash working-directory: ${{ inputs.compose_workdir }} env: DOCKER_IMAGE: ${{ needs.build_and_push.outputs.image_ref }} run: | set -euo pipefail docker compose ${{ inputs.compose_args }} # Windows runners support - name: Compose Up (Windows) if: runner.os == 'Windows' shell: pwsh working-directory: ${{ inputs.compose_workdir }} env: DOCKER_IMAGE: ${{ needs.build_and_push.outputs.image_ref }} run: | docker compose ${{ inputs.compose_args }}