Skip to content

Merge branch 'release/v11.2.1' of https://github.com/utmstack/UTMStac… #117

Merge branch 'release/v11.2.1' of https://github.com/utmstack/UTMStac…

Merge branch 'release/v11.2.1' of https://github.com/utmstack/UTMStac… #117

name: "v11 - Build & Deploy Pipeline"
on:
push:
branches: [ 'release/v11**' ]
release:
types: [ prereleased ]
jobs:
setup_deployment:
name: Setup Deployment
runs-on: ubuntu-24.04
outputs:
tag: ${{ steps.set-env.outputs.tag }}
environment: ${{ steps.set-env.outputs.environment }}
cm_url: ${{ steps.set-env.outputs.cm_url }}
event_processor_tag: ${{ steps.set-env.outputs.event_processor_tag }}
steps:
- name: Determine Build Environment
id: set-env
run: |
# ===================
# DEV Environment
# ===================
# Triggered by: push to release/v11.x.x branches
if ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release/v11') }}; then
ENVIRONMENT="dev"
CM_URL="https://cm.dev.utmstack.com"
echo "Environment: $ENVIRONMENT"
echo "CM URL: $CM_URL"
# Extract version from branch name (e.g., release/v11.2.1 -> v11.2.1)
BRANCH_VERSION=$(echo "${{ github.ref }}" | sed 's|refs/heads/release/||')
echo "Branch version: $BRANCH_VERSION"
# Get latest version from CM
RESPONSE=$(curl -s "${CM_URL}/api/v1/versions/latest")
LATEST_VERSION=$(echo "$RESPONSE" | jq -r '.version // empty')
echo "Latest version from CM: $LATEST_VERSION"
if [ -n "$LATEST_VERSION" ]; then
# Extract base version from latest (e.g., v11.2.1-dev.9 -> v11.2.1)
LATEST_BASE=$(echo "$LATEST_VERSION" | sed 's/-dev\.[0-9]*$//')
echo "Latest base version: $LATEST_BASE"
if [ "$BRANCH_VERSION" = "$LATEST_BASE" ]; then
# Versions match - increment dev number
DEV_NUM=$(echo "$LATEST_VERSION" | grep -oP '(?<=-dev\.)\d+')
NEW_DEV_NUM=$((DEV_NUM + 1))
TAG="${BRANCH_VERSION}-dev.${NEW_DEV_NUM}"
echo "Versions match, incrementing: $TAG"
else
# Versions don't match - CM not updated yet, start at dev.1
TAG="${BRANCH_VERSION}-dev.1"
echo "Versions don't match, starting fresh: $TAG"
fi
else
# No version found in CM - start at dev.1
TAG="${BRANCH_VERSION}-dev.1"
echo "No previous version found, starting fresh: $TAG"
fi
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "environment=$ENVIRONMENT" >> $GITHUB_OUTPUT
echo "cm_url=$CM_URL" >> $GITHUB_OUTPUT
echo "event_processor_tag=${{ vars.TW_EVENT_PROCESSOR_VERSION_DEV }}" >> $GITHUB_OUTPUT
# ===================
# RC Environment
# ===================
# Triggered by: prerelease creation (tag should be v11.x.x)
elif ${{ github.event_name == 'release' && github.event.action == 'prereleased' }}; then
ENVIRONMENT="rc"
CM_URL="https://cm.utmstack.com"
echo "Environment: $ENVIRONMENT"
echo "CM URL: $CM_URL"
# Get the tag from the prerelease event
TAG="${{ github.event.release.tag_name }}"
echo "Tag from prerelease: $TAG"
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "environment=$ENVIRONMENT" >> $GITHUB_OUTPUT
echo "cm_url=$CM_URL" >> $GITHUB_OUTPUT
echo "event_processor_tag=${{ vars.TW_EVENT_PROCESSOR_VERSION_PROD }}" >> $GITHUB_OUTPUT
fi
validations:
name: Validate permissions
runs-on: ubuntu-24.04
needs: setup_deployment
if: ${{ needs.setup_deployment.outputs.tag != '' }}
steps:
- name: Check permissions
run: |
echo "Validating user permissions..."
RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.API_SECRET }}" \
-H "Accept: application/vnd.github.json" \
"https://api.github.com/orgs/utmstack/teams/integration-developers/memberships/${{ github.actor }}")
if echo "$RESPONSE" | grep -q '"state": "active"'; then
echo "✅ User ${{ github.actor }} is a member of the integration-developers team."
else
RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.API_SECRET }}" \
-H "Accept: application/vnd.github.json" \
"https://api.github.com/orgs/utmstack/teams/core-developers/memberships/${{ github.actor }}")
if echo "$RESPONSE" | grep -q '"state": "active"'; then
echo "✅ User ${{ github.actor }} is a member of the core-developers team."
else
echo "⛔ ERROR: User ${{ github.actor }} is not a member of the core-developers or integration-developers team."
echo $RESPONSE
exit 1
fi
fi
check_go_dependencies:
name: Check Go Dependencies
runs-on: ubuntu-24.04
needs: [validations, setup_deployment]
if: ${{ needs.setup_deployment.outputs.tag != '' }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ^1.20
- name: Configure git for private modules
run: |
git config --global url."https://${{ secrets.API_SECRET }}:x-oauth-basic@github.com/".insteadOf "https://github.com/"
echo "GOPRIVATE=github.com/utmstack" >> $GITHUB_ENV
- name: Check for outdated dependencies
run: ${{ github.workspace }}/.github/scripts/golang-updater/golang-updater --check --discover
build_agent:
name: Build and Sign Agent
needs: [check_go_dependencies, setup_deployment]
if: ${{ needs.setup_deployment.outputs.tag != '' }}
runs-on: utmstack-signer
steps:
- name: Check out code into the right branch
uses: actions/checkout@v4
- name: Build Linux Binaries
env:
GOOS: linux
GOARCH: amd64
run: |
cd ${{ github.workspace }}/agent
go build -o utmstack_agent_service -v -ldflags "-X 'github.com/utmstack/UTMStack/agent/config.REPLACE_KEY=${{ secrets.AGENT_SECRET_PREFIX }}'" .
cd ${{ github.workspace }}/agent/updater
go build -o utmstack_updater_service .
- name: Build Windows Binaries (amd64)
env:
GOOS: windows
GOARCH: amd64
run: |
cd ${{ github.workspace }}/agent
go build -o utmstack_agent_service.exe -v -ldflags "-X 'github.com/utmstack/UTMStack/agent/config.REPLACE_KEY=${{ secrets.AGENT_SECRET_PREFIX }}'" .
cd ${{ github.workspace }}/agent/updater
go build -o utmstack_updater_service.exe .
- name: Build Windows Binaries (arm64)
env:
GOOS: windows
GOARCH: arm64
run: |
cd ${{ github.workspace }}/agent
go build -o utmstack_agent_service_arm64.exe -v -ldflags "-X 'github.com/utmstack/UTMStack/agent/config.REPLACE_KEY=${{ secrets.AGENT_SECRET_PREFIX }}'" .
cd ${{ github.workspace }}/agent/updater
go build -o utmstack_updater_service_arm64.exe .
- name: Sign Windows Agents
run: |
cd ${{ github.workspace }}/agent
signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /f "${{ vars.SIGN_CERT }}" /csp "eToken Base Cryptographic Provider" /k "[{{${{ secrets.SIGN_KEY }}}}]=${{ secrets.SIGN_CONTAINER }}" "utmstack_agent_service.exe"
signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /f "${{ vars.SIGN_CERT }}" /csp "eToken Base Cryptographic Provider" /k "[{{${{ secrets.SIGN_KEY }}}}]=${{ secrets.SIGN_CONTAINER }}" "utmstack_agent_service_arm64.exe"
cd ${{ github.workspace }}/agent/updater
signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /f "${{ vars.SIGN_CERT }}" /csp "eToken Base Cryptographic Provider" /k "[{{${{ secrets.SIGN_KEY }}}}]=${{ secrets.SIGN_CONTAINER }}" "utmstack_updater_service.exe"
signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /f "${{ vars.SIGN_CERT }}" /csp "eToken Base Cryptographic Provider" /k "[{{${{ secrets.SIGN_KEY }}}}]=${{ secrets.SIGN_CONTAINER }}" "utmstack_updater_service_arm64.exe"
- name: Upload signed binaries as artifacts
uses: actions/upload-artifact@v4
with:
name: signed-agents
path: |
${{ github.workspace }}/agent/utmstack_agent_service
${{ github.workspace }}/agent/utmstack_agent_service.exe
${{ github.workspace }}/agent/utmstack_agent_service_arm64.exe
${{ github.workspace }}/agent/updater/utmstack_updater_service
${{ github.workspace }}/agent/updater/utmstack_updater_service.exe
${{ github.workspace }}/agent/updater/utmstack_updater_service_arm64.exe
retention-days: 1
build_utmstack_collector:
name: Build UTMStack Collector
needs: [check_go_dependencies, setup_deployment]
if: ${{ needs.setup_deployment.outputs.tag != '' }}
runs-on: ubuntu-24.04
steps:
- name: Check out code into the right branch
uses: actions/checkout@v4
- name: Build UTMStack Collector
run: |
echo "Building UTMStack Collector..."
cd ${{ github.workspace }}/utmstack-collector
GOOS=linux GOARCH=amd64 go build -o utmstack_collector -v -ldflags "-X 'github.com/utmstack/UTMStack/utmstack-collector/config.REPLACE_KEY=${{ secrets.AGENT_SECRET_PREFIX }}'" .
- name: Upload collector binary as artifact
uses: actions/upload-artifact@v4
with:
name: utmstack-collector
path: ${{ github.workspace }}/utmstack-collector/utmstack_collector
retention-days: 1
build_agent_manager:
name: Build Agent Manager Microservice
needs: [build_agent, build_utmstack_collector, setup_deployment]
if: ${{ needs.setup_deployment.outputs.tag != '' }}
runs-on: ubuntu-24.04
steps:
- name: Check out code into the right branch
uses: actions/checkout@v4
- name: Download signed agents from artifacts
uses: actions/download-artifact@v4
with:
name: signed-agents
path: ${{ github.workspace }}/agent
- name: Download UTMStack Collector from artifacts
uses: actions/download-artifact@v4
with:
name: utmstack-collector
path: ${{ github.workspace }}/utmstack-collector
- name: Prepare dependencies for Agent Manager Image
run: |
cd ${{ github.workspace }}/agent-manager
GOOS=linux GOARCH=amd64 go build -o agent-manager -v .
mkdir -p ./dependencies/collector
curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/collector/linux-as400-collector.zip" -o ./dependencies/collector/linux-as400-collector.zip
curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/collector/windows-as400-collector.zip" -o ./dependencies/collector/windows-as400-collector.zip
cp "${{ github.workspace }}/utmstack-collector/utmstack_collector" ./dependencies/collector/
cp "${{ github.workspace }}/utmstack-collector/version.json" ./dependencies/collector/
mkdir -p ./dependencies/agent/
curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/agent/utmstack_agent_dependencies_linux.zip" -o ./dependencies/agent/utmstack_agent_dependencies_linux.zip
curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/agent/utmstack_agent_dependencies_windows.zip" -o ./dependencies/agent/utmstack_agent_dependencies_windows.zip
curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/agent/utmstack_agent_dependencies_windows_arm64.zip" -o ./dependencies/agent/utmstack_agent_dependencies_windows_arm64.zip
curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/agent/utmstack-macos-agent-v10.pkg" -o ./dependencies/agent/utmstack-macos-agent.pkg
cp "${{ github.workspace }}/agent/utmstack_agent_service" ./dependencies/agent/
cp "${{ github.workspace }}/agent/utmstack_agent_service.exe" ./dependencies/agent/
cp "${{ github.workspace }}/agent/utmstack_agent_service_arm64.exe" ./dependencies/agent/
cp "${{ github.workspace }}/agent/updater/utmstack_updater_service" ./dependencies/agent/
cp "${{ github.workspace }}/agent/updater/utmstack_updater_service.exe" ./dependencies/agent/
cp "${{ github.workspace }}/agent/updater/utmstack_updater_service_arm64.exe" ./dependencies/agent/
cp "${{ github.workspace }}/agent/version.json" ./dependencies/agent/
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: utmstack
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push the Agent Manager Image
uses: docker/build-push-action@v6
with:
context: ./agent-manager
push: true
tags: ghcr.io/utmstack/utmstack/agent-manager:${{ needs.setup_deployment.outputs.tag }}
build_event_processor:
name: Build Event Processor Microservice
needs: [check_go_dependencies, setup_deployment]
if: ${{ needs.setup_deployment.outputs.tag != '' }}
runs-on: ubuntu-24.04
steps:
- name: Check out code into the right branch
uses: actions/checkout@v4
- name: Build Plugins
env:
GOOS: linux
GOARCH: amd64
run: |
cd ${{ github.workspace }}/plugins/alerts; go build -o com.utmstack.alerts.plugin -v .
cd ${{ github.workspace }}/plugins/aws; go build -o com.utmstack.aws.plugin -v .
cd ${{ github.workspace }}/plugins/azure; go build -o com.utmstack.azure.plugin -v .
cd ${{ github.workspace }}/plugins/bitdefender; go build -o com.utmstack.bitdefender.plugin -v .
cd ${{ github.workspace }}/plugins/config; go build -o com.utmstack.config.plugin -v .
cd ${{ github.workspace }}/plugins/events; go build -o com.utmstack.events.plugin -v .
cd ${{ github.workspace }}/plugins/gcp; go build -o com.utmstack.gcp.plugin -v .
cd ${{ github.workspace }}/plugins/geolocation; go build -o com.utmstack.geolocation.plugin -v .
cd ${{ github.workspace }}/plugins/inputs; go build -o com.utmstack.inputs.plugin -v .
cd ${{ github.workspace }}/plugins/o365; go build -o com.utmstack.o365.plugin -v .
cd ${{ github.workspace }}/plugins/sophos; go build -o com.utmstack.sophos.plugin -v .
cd ${{ github.workspace }}/plugins/stats; go build -o com.utmstack.stats.plugin -v .
cd ${{ github.workspace }}/plugins/soc-ai; go build -o com.utmstack.soc-ai.plugin -v .
cd ${{ github.workspace }}/plugins/modules-config; go build -o com.utmstack.modules-config.plugin -v .
cd ${{ github.workspace }}/plugins/crowdStrike; go build -o com.utmstack.crowdstrike.plugin -v .
cd ${{ github.workspace }}/plugins/threadwinds-ingestion; go build -o com.utmstack.threadwinds-ingestion.plugin -v .
- name: Prepare Dependencies for Event Processor Image
run: |
mkdir -p ./geolocation
curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/geolocation/asn-blocks-v4.csv" -o ./geolocation/asn-blocks-v4.csv
curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/geolocation/asn-blocks-v6.csv" -o ./geolocation/asn-blocks-v6.csv
curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/geolocation/blocks-v4.csv" -o ./geolocation/blocks-v4.csv
curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/geolocation/blocks-v6.csv" -o ./geolocation/blocks-v6.csv
curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/geolocation/locations-en.csv" -o ./geolocation/locations-en.csv
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: utmstack
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push the Event Processor Image
uses: docker/build-push-action@v6
with:
context: .
file: ./event_processor.Dockerfile
push: true
tags: ghcr.io/utmstack/utmstack/eventprocessor:${{ needs.setup_deployment.outputs.tag }}
build-args: |
BASE_IMAGE=ghcr.io/threatwinds/eventprocessor/base:${{ needs.setup_deployment.outputs.event_processor_tag }}
build_backend:
name: Build Backend Microservice
needs: [validations, setup_deployment]
if: ${{ needs.setup_deployment.outputs.tag != '' }}
uses: ./.github/workflows/reusable-java.yml
with:
image_name: backend
tag: ${{ needs.setup_deployment.outputs.tag }}
java_version: '17'
use_tag_as_version: true
maven_profile: 'prod'
maven_goals: 'clean package'
build_frontend:
name: Build Frontend Microservice
needs: [validations, setup_deployment]
if: ${{ needs.setup_deployment.outputs.tag != '' }}
uses: ./.github/workflows/reusable-node.yml
with:
image_name: frontend
tag: ${{ needs.setup_deployment.outputs.tag }}
build_user_auditor:
name: Build User-Auditor Microservice
needs: [validations, setup_deployment]
if: ${{ needs.setup_deployment.outputs.tag != '' }}
uses: ./.github/workflows/reusable-java.yml
with:
image_name: user-auditor
tag: ${{ needs.setup_deployment.outputs.tag }}
java_version: '11'
use_version_file: false
maven_goals: 'clean install -U'
build_web_pdf:
name: Build Web-PDF Microservice
needs: [validations, setup_deployment]
if: ${{ needs.setup_deployment.outputs.tag != '' }}
uses: ./.github/workflows/reusable-java.yml
with:
image_name: web-pdf
tag: ${{ needs.setup_deployment.outputs.tag }}
java_version: '11'
use_version_file: false
maven_goals: 'clean install -U'
all_builds_complete:
name: All Builds Complete
needs: [
build_agent_manager,
build_event_processor,
build_backend,
build_frontend,
build_user_auditor,
build_web_pdf
]
runs-on: ubuntu-24.04
steps:
- run: echo "✅ All builds completed successfully."
generate_changelog:
name: Generate Changelog
needs: [all_builds_complete, setup_deployment]
if: ${{ needs.setup_deployment.outputs.tag != '' && needs.setup_deployment.outputs.environment == 'rc' }}
uses: ./.github/workflows/generate-changelog.yml
with:
current_tag: ${{ needs.setup_deployment.outputs.tag }}
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
build_installer_rc:
name: Build & Upload Installer (RC)
needs: [generate_changelog, setup_deployment]
if: ${{ needs.setup_deployment.outputs.tag != '' && needs.setup_deployment.outputs.environment == 'rc' }}
uses: ./.github/workflows/installer-release.yml
with:
version: ${{ needs.setup_deployment.outputs.tag }}
version_major: v11
environment: rc
prerelease: true
changelog: ${{ needs.generate_changelog.outputs.changelog }}
secrets:
API_SECRET: ${{ secrets.API_SECRET }}
CM_ENCRYPT_SALT: ${{ secrets.CM_ENCRYPT_SALT }}
CM_SIGN_PUBLIC_KEY: ${{ secrets.CM_SIGN_PUBLIC_KEY }}
deploy_installer_dev:
name: Deploy Installer (Dev)
needs: [all_builds_complete, setup_deployment]
if: ${{ needs.setup_deployment.outputs.tag != '' && needs.setup_deployment.outputs.environment == 'dev' }}
uses: ./.github/workflows/installer-release.yml
with:
version: ${{ needs.setup_deployment.outputs.tag }}
version_major: v11
environment: dev
secrets:
API_SECRET: ${{ secrets.API_SECRET }}
CM_ENCRYPT_SALT: ${{ secrets.CM_ENCRYPT_SALT }}
CM_SIGN_PUBLIC_KEY: ${{ secrets.CM_SIGN_PUBLIC_KEY }}
publish_new_version:
name: Publish New Version to Customer Manager
needs: [all_builds_complete, generate_changelog, setup_deployment]
if: ${{ always() && needs.all_builds_complete.result == 'success' && needs.setup_deployment.outputs.tag != '' }}
runs-on: ubuntu-24.04
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish version
env:
CHANGELOG_CONTENT: ${{ needs.generate_changelog.outputs.changelog }}
ENVIRONMENT: ${{ needs.setup_deployment.outputs.environment }}
TAG: ${{ needs.setup_deployment.outputs.tag }}
CM_URL: ${{ needs.setup_deployment.outputs.cm_url }}
run: |
# Use AI changelog for rc, generic for dev
if [ "$ENVIRONMENT" = "rc" ] && [ -n "$CHANGELOG_CONTENT" ]; then
changelog="$CHANGELOG_CONTENT"
else
changelog="Development build $TAG - Internal testing release"
fi
echo "Environment: $ENVIRONMENT"
echo "CM URL: $CM_URL"
echo "Tag: $TAG"
# Select CM_SERVICE_ACCOUNT based on environment
if [ "$ENVIRONMENT" = "dev" ]; then
cmAuth=$(echo '${{ secrets.CM_SERVICE_ACCOUNT_DEV }}' | jq -r '.')
else
cmAuth=$(echo '${{ secrets.CM_SERVICE_ACCOUNT_PROD }}' | jq -r '.')
fi
id=$(echo "$cmAuth" | jq -r '.id')
key=$(echo "$cmAuth" | jq -r '.key')
body=$(jq -n \
--arg version "$TAG" \
--arg changelog "$changelog" \
'{version: $version, changelog: $changelog}'
)
response=$(curl -s -X POST "${CM_URL}/api/v1/versions/register" \
-H "Content-Type: application/json" \
-H "id: $id" \
-H "key: $key" \
-d "$body")
echo "Response: $response"
schedule:
name: Schedule release to our instances
needs: [publish_new_version, setup_deployment]
if: ${{ always() && needs.publish_new_version.result == 'success' && needs.setup_deployment.outputs.tag != '' }}
runs-on: ubuntu-24.04
env:
ENVIRONMENT: ${{ needs.setup_deployment.outputs.environment }}
TAG: ${{ needs.setup_deployment.outputs.tag }}
CM_URL: ${{ needs.setup_deployment.outputs.cm_url }}
steps:
- name: Schedule updates
run: |
echo "🔍 Environment: $ENVIRONMENT"
echo "🔍 Version: $TAG"
echo "🔍 CM URL: $CM_URL"
# Select instance IDs and auth based on environment
if [ "$ENVIRONMENT" = "dev" ]; then
instance_ids="${{ vars.SCHEDULE_INSTANCES_DEV }}"
auth_json='${{ secrets.CM_SERVICE_ACCOUNT_DEV }}'
else
# rc uses prod variables
instance_ids="${{ vars.SCHEDULE_INSTANCES_PROD }}"
auth_json='${{ secrets.CM_SERVICE_ACCOUNT_PROD }}'
fi
# Extract id and key from auth JSON
auth_id=$(echo "$auth_json" | jq -r '.id')
auth_key=$(echo "$auth_json" | jq -r '.key')
# Parse IDs (handle single ID or comma-separated IDs)
IFS=',' read -ra ID_ARRAY <<< "$instance_ids"
# Iterate over each instance ID
for instance_id in "${ID_ARRAY[@]}"; do
instance_id=$(echo "$instance_id" | xargs)
echo "📅 Scheduling release for instance: $instance_id"
response=$(curl -s -w "\n%{http_code}" -X POST "${CM_URL}/api/v1/updates" \
-H "Content-Type: application/json" \
-H "id: $auth_id" \
-H "key: $auth_key" \
-d "{\"instance_id\": \"$instance_id\", \"version\": \"$TAG\"}")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
echo "✅ Successfully scheduled for instance: $instance_id"
else
echo "❌ Failed to schedule for instance: $instance_id (HTTP $http_code)"
echo "Response: $body"
exit 1
fi
done
echo "✅ Scheduled release for all instances with version $TAG"