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: description: "Image repository name under harbor_project (example: myapp)" type: string required: true context: type: string default: . dockerfile: type: string default: Dockerfile platforms: type: string default: linux/amd64,linux/arm64 deploy_runners_json: description: "JSON array of runner labels (example: [\"prod-1\",\"prod-2\"])" type: string required: true compose_workdir: type: string default: . compose_args: description: "Arguments after `docker compose`" 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 } outputs: image_repo: description: "Image repository (no tag/digest), e.g. harbor.hcmc.online/ci/myapp" value: ${{ jobs.build_and_push.outputs.image_repo }} image_digest: description: "Content digest, e.g. sha256:..." value: ${{ jobs.build_and_push.outputs.image_digest }} image_ref: description: "Immutable image ref, e.g. harbor.hcmc.online/ci/myapp@sha256:..." value: ${{ jobs.build_and_push.outputs.image_ref }} jobs: 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-qemu-action@v3 - 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)" RAW_BRANCH="${{ github.ref_name }}" # Docker tags cannot contain slashes, spaces, etc. BRANCH="$(echo "${RAW_BRANCH}" | tr "/\\" "-" | tr -c "A-Za-z0-9_.-" "-" | sed -E "s/^-+|-+$//g" | cut -c1-120)" if [[ -z "${BRANCH}" ]]; then BRANCH="branch"; fi { echo "image_repo=$IMAGE" echo "sha_tag=sha-$SHORT_SHA" 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 }} - 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: 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 }}" - 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 }} - 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 }}