[v0.165.3] Docker Image Build and Push (Production) #600
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Unstract Docker Image Build and Push (Production) | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Docker image tag" | |
| required: true | |
| set_as_latest: | |
| description: "Set as latest release" | |
| type: boolean | |
| default: false | |
| required: false | |
| release: | |
| types: | |
| - created | |
| run-name: "[${{ github.event.release.tag_name || github.event.inputs.tag }}] Docker Image Build and Push (Production)" | |
| jobs: | |
| # Validates tag format and version monotonicity within the same major.minor line. | |
| # This guards the workflow_dispatch tag input and any release event. | |
| # The primary defense against bad tags should be a GitHub tag protection ruleset | |
| # restricting v* tag creation to the release bot; this job is the in-workflow | |
| # safety net that runs regardless. | |
| validate-tag: | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| shell: bash -euo pipefail {0} | |
| steps: | |
| - name: Determine tag | |
| id: get-tag | |
| env: | |
| RELEASE_TAG: ${{ github.event.release.tag_name }} | |
| INPUT_TAG: ${{ github.event.inputs.tag }} | |
| run: | | |
| TAG="${RELEASE_TAG:-$INPUT_TAG}" | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| - name: Validate tag format | |
| env: | |
| TAG: ${{ steps.get-tag.outputs.tag }} | |
| run: | | |
| if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "::error::Tag '$TAG' does not match expected format 'vX.Y.Z'." | |
| exit 1 | |
| fi | |
| echo "Tag format valid: $TAG" | |
| - name: Validate version increment (same-line) | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| NEW_TAG: ${{ steps.get-tag.outputs.tag }} | |
| run: | | |
| # Find the previous release on the same major.minor line. This handles | |
| # both normal releases (next patch/minor on main) and hotfixes on older | |
| # lines, which are NOT marked as "latest" and would otherwise compare | |
| # incorrectly against the unrelated main-line latest. | |
| NEW_VER="${NEW_TAG#v}" | |
| NEW_MAJOR_MINOR=$(echo "$NEW_VER" | cut -d. -f1-2) | |
| PREV_TAG=$(gh release list --repo "${{ github.repository }}" \ | |
| --limit 100 --exclude-drafts --exclude-pre-releases \ | |
| --json tagName \ | |
| --jq "[.[] | select(.tagName != \"$NEW_TAG\" and (.tagName | startswith(\"v${NEW_MAJOR_MINOR}.\")))] | .[0].tagName // empty") | |
| if [[ -z "$PREV_TAG" ]]; then | |
| echo "No prior release in line v${NEW_MAJOR_MINOR}.x (new minor/major bump). Skipping increment check." | |
| exit 0 | |
| fi | |
| if [[ ! "$PREV_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "::error::Previous tag '$PREV_TAG' has unexpected format. Refusing to compare." | |
| exit 1 | |
| fi | |
| echo "Previous release on line: $PREV_TAG" | |
| echo "New release: $NEW_TAG" | |
| PREV_VER="${PREV_TAG#v}" | |
| if [[ "$PREV_VER" == "$NEW_VER" ]]; then | |
| echo "::error::New version $NEW_TAG equals previous version $PREV_TAG on the same line." | |
| exit 1 | |
| fi | |
| if ! printf '%s\n%s\n' "$PREV_VER" "$NEW_VER" | sort -V -C; then | |
| echo "::error::New version $NEW_TAG is not greater than previous version $PREV_TAG on line v${NEW_MAJOR_MINOR}.x." | |
| exit 1 | |
| fi | |
| echo "Version increment valid: $PREV_TAG -> $NEW_TAG" | |
| build-and-push: | |
| needs: validate-tag | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| service_name: | |
| [ | |
| backend, | |
| frontend, | |
| platform-service, | |
| prompt-service, | |
| runner, | |
| worker-unified, | |
| x2text-service, | |
| ] | |
| steps: | |
| - name: Checkout code for release | |
| if: github.event_name == 'release' | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.release.tag_name }} | |
| - name: Checkout code for branch | |
| if: github.event_name != 'release' | |
| uses: actions/checkout@v6 | |
| # Set up QEMU for ARM64 emulation | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@v4 | |
| with: | |
| platforms: linux/amd64,linux/arm64/v8 | |
| # Set up Docker Buildx for better caching and multi-arch builds | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| with: | |
| platforms: linux/amd64,linux/arm64/v8 | |
| # Log in to Docker Hub | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@v4 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| # Set version tag based on event type. Uses env: to avoid interpolating | |
| # user input directly into the run block. | |
| - name: Set version tag | |
| id: set-tag | |
| env: | |
| RELEASE_TAG: ${{ github.event.release.tag_name }} | |
| INPUT_TAG: ${{ github.event.inputs.tag }} | |
| run: echo "DOCKER_VERSION_TAG=${RELEASE_TAG:-$INPUT_TAG}" >> "$GITHUB_ENV" | |
| # Set up additional tags for release builds | |
| - name: Set image tags | |
| id: tags | |
| run: | | |
| # Check if service exists in the config | |
| echo "Checking if service ${{ matrix.service_name }} exists in docker-compose.build.yaml" | |
| if ! grep -q "^ ${{ matrix.service_name }}:" ./docker/docker-compose.build.yaml; then | |
| echo "Service ${{ matrix.service_name }} not found in docker-compose.build.yaml" && exit 1 | |
| fi | |
| # Set latest tag for releases or when explicitly requested | |
| echo "SEMVER_IMAGE_TAG=unstract/${{ matrix.service_name }}:${{ env.DOCKER_VERSION_TAG }}" >> $GITHUB_ENV | |
| # Set latest tag if it's a release or if set_as_latest is true | |
| if [ "${{ github.event_name }}" = "release" ] || [ "${{ github.event.inputs.set_as_latest }}" = "true" ]; then | |
| echo "LATEST_IMAGE_TAG=unstract/${{ matrix.service_name }}:latest" >> $GITHUB_ENV | |
| else | |
| echo "LATEST_IMAGE_TAG=" >> $GITHUB_ENV | |
| fi | |
| # Build and push using Docker Bake | |
| - name: Build and push image | |
| uses: docker/bake-action@v7 | |
| env: | |
| VERSION: ${{ env.DOCKER_VERSION_TAG }} | |
| with: | |
| files: ./docker/docker-compose.build.yaml | |
| targets: ${{ matrix.service_name }} | |
| push: true | |
| set: | | |
| *.tags=${{ env.SEMVER_IMAGE_TAG }} | |
| ${{ env.LATEST_IMAGE_TAG && format('*.tags={0}', env.LATEST_IMAGE_TAG) || '' }} | |
| *.context=. | |
| *.args.VERSION=${{ env.DOCKER_VERSION_TAG }} | |
| *.platform=linux/amd64,linux/arm64/v8 | |
| *.cache-from=type=gha,scope=${{ matrix.service_name }} | |
| *.cache-to=type=gha,mode=max,scope=${{ matrix.service_name }} | |
| # Capture build result and write to artifact | |
| - name: Write build status | |
| if: always() | |
| run: | | |
| mkdir -p build-status | |
| cat > build-status/${{ matrix.service_name }}.json << EOF | |
| { | |
| "service": "${{ matrix.service_name }}", | |
| "status": "${{ job.status }}", | |
| "tag": "${{ env.DOCKER_VERSION_TAG }}" | |
| } | |
| EOF | |
| # Upload status for summary job | |
| - name: Upload build status | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: build-status-${{ matrix.service_name }} | |
| path: build-status/${{ matrix.service_name }}.json | |
| retention-days: 1 | |
| # Summary job that runs after all builds. Depends on validate-tag so it doesn't | |
| # produce misleading output when validation blocked the build. | |
| build-summary: | |
| needs: [validate-tag, build-and-push] | |
| runs-on: ubuntu-latest | |
| if: always() && needs.validate-tag.result == 'success' | |
| steps: | |
| # Download all build status artifacts | |
| - name: Download build statuses | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: build-status-* | |
| merge-multiple: true | |
| path: build-status | |
| - name: Generate build summary | |
| id: summary | |
| run: | | |
| # Initialize variables | |
| TOTAL_SERVICES=7 | |
| OVERALL_RESULT='${{ needs.build-and-push.result }}' | |
| SUCCESS_COUNT=0 | |
| FAILED_COUNT=0 | |
| FAILED_SERVICES="" | |
| SUCCESS_SERVICES="" | |
| # Process individual service results if artifacts exist | |
| if [ -d "build-status" ]; then | |
| for status_file in build-status/*.json; do | |
| if [ -f "$status_file" ]; then | |
| SERVICE=$(jq -r '.service' "$status_file") | |
| STATUS=$(jq -r '.status' "$status_file") | |
| if [ "$STATUS" = "success" ]; then | |
| SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) | |
| SUCCESS_SERVICES="${SUCCESS_SERVICES}${SERVICE}, " | |
| else | |
| FAILED_COUNT=$((FAILED_COUNT + 1)) | |
| FAILED_SERVICES="${FAILED_SERVICES}${SERVICE}, " | |
| fi | |
| fi | |
| done | |
| # Trim trailing comma and space | |
| SUCCESS_SERVICES=${SUCCESS_SERVICES%, } | |
| FAILED_SERVICES=${FAILED_SERVICES%, } | |
| fi | |
| # Set overall status based on results | |
| if [ "$OVERALL_RESULT" = "success" ]; then | |
| SUMMARY_STATUS="✅ All Docker builds successful" | |
| SUMMARY_COLOR="good" | |
| ALL_SUCCESS="true" | |
| elif [ "$OVERALL_RESULT" = "cancelled" ]; then | |
| SUMMARY_STATUS="⚠️ Docker builds cancelled" | |
| SUMMARY_COLOR="warning" | |
| ALL_SUCCESS="false" | |
| else | |
| SUMMARY_STATUS="❌ Some Docker builds failed" | |
| SUMMARY_COLOR="danger" | |
| ALL_SUCCESS="false" | |
| fi | |
| echo "summary_status=$SUMMARY_STATUS" >> $GITHUB_OUTPUT | |
| echo "summary_color=$SUMMARY_COLOR" >> $GITHUB_OUTPUT | |
| echo "total_count=$TOTAL_SERVICES" >> $GITHUB_OUTPUT | |
| echo "all_success=$ALL_SUCCESS" >> $GITHUB_OUTPUT | |
| echo "overall_result=$OVERALL_RESULT" >> $GITHUB_OUTPUT | |
| echo "success_count=$SUCCESS_COUNT" >> $GITHUB_OUTPUT | |
| echo "failed_count=$FAILED_COUNT" >> $GITHUB_OUTPUT | |
| echo "failed_services=$FAILED_SERVICES" >> $GITHUB_OUTPUT | |
| echo "success_services=$SUCCESS_SERVICES" >> $GITHUB_OUTPUT | |
| # Create a concise summary for Slack | |
| if [ "$ALL_SUCCESS" = "true" ]; then | |
| SLACK_DETAILS="All $TOTAL_SERVICES services built successfully ✅" | |
| else | |
| SLACK_DETAILS="❌ Failed: $FAILED_SERVICES | ✅ Succeeded: $SUCCESS_SERVICES" | |
| fi | |
| echo "slack_details=$SLACK_DETAILS" >> $GITHUB_OUTPUT | |
| # Write to GitHub Summary | |
| - name: Write GitHub Summary | |
| run: | | |
| echo "# 🐳 Docker Build Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Status**: ${{ steps.summary.outputs.summary_status }}" >> $GITHUB_STEP_SUMMARY | |
| echo "**Tag**: ${{ github.event.release.tag_name || github.event.inputs.tag }}" >> $GITHUB_STEP_SUMMARY | |
| echo "**Total Services**: ${{ steps.summary.outputs.total_count }}" >> $GITHUB_STEP_SUMMARY | |
| # Show counts if available | |
| if [ -n "${{ steps.summary.outputs.success_count }}" ] && [ "${{ steps.summary.outputs.success_count }}" != "0" ]; then | |
| echo "**Successful**: ${{ steps.summary.outputs.success_count }}/${{ steps.summary.outputs.total_count }}" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [ -n "${{ steps.summary.outputs.failed_count }}" ] && [ "${{ steps.summary.outputs.failed_count }}" != "0" ]; then | |
| echo "**Failed**: ${{ steps.summary.outputs.failed_count }}/${{ steps.summary.outputs.total_count }}" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Show service status table | |
| echo "## Service Build Status" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Process and display individual service statuses | |
| if [ -d "build-status" ]; then | |
| echo "| Service | Status |" >> $GITHUB_STEP_SUMMARY | |
| echo "|---------|--------|" >> $GITHUB_STEP_SUMMARY | |
| # Define services in order | |
| for service in backend frontend platform-service prompt-service runner worker-unified x2text-service; do | |
| if [ -f "build-status/${service}.json" ]; then | |
| STATUS=$(jq -r '.status' "build-status/${service}.json") | |
| if [ "$STATUS" = "success" ]; then | |
| echo "| ${service} | ✅ Success |" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "| ${service} | ❌ Failed |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| else | |
| echo "| ${service} | ⚠️ Unknown |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| done | |
| else | |
| # Fallback if no artifacts (shouldn't happen but just in case) | |
| if [ "${{ steps.summary.outputs.all_success }}" = "true" ]; then | |
| echo "All services built successfully! ✅" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "⚠️ Build workflow result: **${{ steps.summary.outputs.overall_result }}**" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Unable to determine individual service status. Please check the job logs above." >> $GITHUB_STEP_SUMMARY | |
| fi | |
| fi | |
| # Show failed services if any | |
| if [ -n "${{ steps.summary.outputs.failed_services }}" ]; then | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "## ❌ Failed Services" >> $GITHUB_STEP_SUMMARY | |
| echo "${{ steps.summary.outputs.failed_services }}" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "---" >> $GITHUB_STEP_SUMMARY | |
| echo "[View Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY | |
| # Send summary notification to Slack | |
| - name: Send Slack summary notification | |
| if: always() | |
| uses: slackapi/slack-github-action@v3.0.1 | |
| with: | |
| webhook-type: incoming-webhook | |
| payload: | | |
| { | |
| "text": "${{ steps.summary.outputs.summary_status }}", | |
| "attachments": [ | |
| { | |
| "color": "${{ steps.summary.outputs.summary_color }}", | |
| "fields": [ | |
| { | |
| "title": "Docker Tag", | |
| "value": "${{ github.event.release.tag_name || github.event.inputs.tag }}", | |
| "short": true | |
| }, | |
| { | |
| "title": "Total Services", | |
| "value": "${{ steps.summary.outputs.total_count }}", | |
| "short": true | |
| }, | |
| { | |
| "title": "Repository", | |
| "value": "${{ github.repository }}", | |
| "short": true | |
| }, | |
| { | |
| "title": "Workflow", | |
| "value": "${{ github.workflow }}", | |
| "short": true | |
| }, | |
| { | |
| "title": "Build Details", | |
| "value": "${{ steps.summary.outputs.slack_details }}", | |
| "short": false | |
| }, | |
| { | |
| "title": "Workflow Run", | |
| "value": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", | |
| "short": false | |
| } | |
| ], | |
| "footer": "GitHub Actions" | |
| } | |
| ] | |
| } | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} |