Skip to content

Commit 1883416

Browse files
owineclaude
andcommitted
refactor(deploy): enhance stack detection to categorize removed/existing/new stacks
Consolidates stack detection into a unified script that categorizes all stacks as removed, existing, or new. This enables sequential deployment (existing stacks first, then new stacks) while maintaining removal cleanup. Changes: - Replace detect-removed-stacks.sh with detect-stack-changes.sh (unified detection) - Detect three stack categories: removed, existing, new - Use "removed" terminology instead of "deleted" throughout - Update deploy.yml outputs: deleted_stacks → removed_stacks - Update Discord notifications to reflect removed stacks - Sequential deployment: existing stacks → new stacks Benefits: - Single source of truth for stack categorization - Transactional detection ensures consistency - Clear terminology (removed vs deleted) - Enables different deployment strategies per category Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent eba4185 commit 1883416

File tree

3 files changed

+543
-351
lines changed

3 files changed

+543
-351
lines changed

.github/workflows/deploy.yml

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ name: Deploy Docker Compose
66
# - lib/common.sh: Logging, validation, and utility functions
77
# - health-check.sh: Service health verification with stack-specific counting
88
# - deploy-stacks.sh: Parallel deployment with comprehensive error handling
9-
# - detect-removed-stacks.sh: Multi-method stack removal detection
9+
# - detect-stack-changes.sh: Multi-method detection for removed/existing/new stacks
1010
# - cleanup-stack.sh: Individual stack cleanup helper
1111
# - rollback-stacks.sh: Automated rollback with stack discovery
1212
#
@@ -112,7 +112,7 @@ jobs:
112112
previous_sha: ${{ steps.backup.outputs.previous_sha }}
113113
deployment_needed: ${{ steps.backup.outputs.deployment_needed }}
114114
deleted_files: ${{ steps.changed-files.outputs.deleted_files }}
115-
deploy_status: ${{ steps.deploy.outcome }}
115+
deploy_status: ${{ (steps.deploy-existing.outcome == 'success' || steps.deploy-existing.outcome == 'skipped') && (steps.deploy-new.outcome == 'success' || steps.deploy-new.outcome == 'skipped') && 'success' || 'failure' }}
116116
health_status: ${{ steps.health.outcome }}
117117
cleanup_status: ${{ steps.cleanup.outcome }}
118118
rollback_status: ${{ steps.rollback.outcome }}
@@ -130,8 +130,12 @@ jobs:
130130
rollback_total_containers: ${{ steps.rollback-health.outputs.rollback_total_containers }}
131131
rollback_running_containers: ${{ steps.rollback-health.outputs.rollback_running_containers }}
132132
rollback_success_rate: ${{ steps.rollback-health.outputs.rollback_success_rate }}
133-
removed_stacks: ${{ steps.cleanup-removed.outputs.removed_stacks }}
134-
has_removed_stacks: ${{ steps.cleanup-removed.outputs.has_removed_stacks }}
133+
removed_stacks: ${{ steps.detect-changes.outputs.removed_stacks }}
134+
existing_stacks: ${{ steps.detect-changes.outputs.existing_stacks }}
135+
new_stacks: ${{ steps.detect-changes.outputs.new_stacks }}
136+
has_removed_stacks: ${{ steps.detect-changes.outputs.has_removed_stacks }}
137+
has_existing_stacks: ${{ steps.detect-changes.outputs.has_existing_stacks }}
138+
has_new_stacks: ${{ steps.detect-changes.outputs.has_new_stacks }}
135139
steps:
136140
- name: Validate and sanitize inputs
137141
run: |
@@ -473,28 +477,29 @@ jobs:
473477
#
474478
# Design: docs/plans/2025-12-06-enhanced-stack-removal-detection-design.md
475479

476-
- name: Detect and clean up removed stacks
477-
id: cleanup-removed
480+
- name: Detect stack changes (removed/existing/new)
481+
id: detect-changes
478482
if: steps.backup.outputs.deployment_needed == 'true'
479483
continue-on-error: false
480484
run: |
481-
./.compose-workflow/scripts/deployment/detect-removed-stacks.sh \
485+
./.compose-workflow/scripts/deployment/detect-stack-changes.sh \
482486
--current-sha "${{ steps.backup.outputs.previous_sha }}" \
483487
--target-ref "${{ inputs.target-ref }}" \
484-
--deleted-files '${{ steps.changed-files.outputs.deleted_files }}' \
488+
--input-stacks '${{ inputs.stacks }}' \
489+
--removed-files '${{ steps.changed-files.outputs.deleted_files }}' \
485490
--ssh-user "${{ secrets.SSH_USER }}" \
486491
--ssh-host "${{ secrets.SSH_HOST }}"
487492
488493
- name: Notify removed stacks cleanup
489-
if: steps.cleanup-removed.outputs.has_removed_stacks == 'true'
494+
if: steps.detect-changes.outputs.has_removed_stacks == 'true'
490495
run: |
491496
echo "📢 Sending cleanup notification to Discord..."
492497
493498
# Get webhook URL from 1Password
494499
WEBHOOK_URL=$(op read "${{ inputs.webhook-url }}")
495500
496501
# Build removed stacks list and create JSON payload using jq for proper escaping
497-
REMOVED_STACKS='${{ steps.cleanup-removed.outputs.removed_stacks }}'
502+
REMOVED_STACKS='${{ steps.detect-changes.outputs.removed_stacks }}'
498503
STACK_LIST=$(echo "$REMOVED_STACKS" | jq -r '.[] | "- " + .')
499504
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
500505
@@ -553,13 +558,34 @@ jobs:
553558
--startup-timeout "${{ inputs.service-startup-timeout }}" \
554559
--compose-args "${{ inputs.args || '' }}"
555560
556-
- name: Deploy All Stacks
557-
id: deploy
558-
if: steps.backup.outputs.deployment_needed == 'true' && (inputs.has-dockge == false || steps.deploy-dockge.outcome == 'success')
561+
- name: Deploy Existing Stacks
562+
id: deploy-existing
563+
if: steps.backup.outputs.deployment_needed == 'true' && steps.detect-changes.outputs.has_existing_stacks == 'true' && (inputs.has-dockge == false || steps.deploy-dockge.outcome == 'success')
559564
continue-on-error: true
560565
run: |
566+
echo "🔄 Deploying existing stacks (updates)..."
561567
./.compose-workflow/scripts/deployment/deploy-stacks.sh \
562-
--stacks "${{ join(fromJSON(inputs.stacks), ' ') }}" \
568+
--stacks "${{ join(fromJSON(steps.detect-changes.outputs.existing_stacks), ' ') }}" \
569+
--target-ref "${{ inputs.target-ref }}" \
570+
--compose-args "${{ inputs.args || '' }}" \
571+
--ssh-user "${{ secrets.SSH_USER }}" \
572+
--ssh-host "${{ secrets.SSH_HOST }}" \
573+
--op-token "${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}" \
574+
--git-fetch-timeout "${{ inputs.git-fetch-timeout }}" \
575+
--git-checkout-timeout "${{ inputs.git-checkout-timeout }}" \
576+
--image-pull-timeout "${{ inputs.image-pull-timeout }}" \
577+
--service-startup-timeout "${{ inputs.service-startup-timeout }}" \
578+
--validation-env-timeout "${{ inputs.validation-env-timeout }}" \
579+
--validation-syntax-timeout "${{ inputs.validation-syntax-timeout }}"
580+
581+
- name: Deploy New Stacks
582+
id: deploy-new
583+
if: steps.backup.outputs.deployment_needed == 'true' && steps.detect-changes.outputs.has_new_stacks == 'true' && (inputs.has-dockge == false || steps.deploy-dockge.outcome == 'success') && (steps.detect-changes.outputs.has_existing_stacks == 'false' || steps.deploy-existing.outcome == 'success')
584+
continue-on-error: true
585+
run: |
586+
echo "✨ Deploying new stacks (fresh deployments)..."
587+
./.compose-workflow/scripts/deployment/deploy-stacks.sh \
588+
--stacks "${{ join(fromJSON(steps.detect-changes.outputs.new_stacks), ' ') }}" \
563589
--target-ref "${{ inputs.target-ref }}" \
564590
--compose-args "${{ inputs.args || '' }}" \
565591
--ssh-user "${{ secrets.SSH_USER }}" \
@@ -574,7 +600,7 @@ jobs:
574600
575601
- name: Health Check All Services
576602
id: health
577-
if: steps.backup.outputs.deployment_needed == 'true' && steps.deploy.outcome == 'success'
603+
if: steps.backup.outputs.deployment_needed == 'true' && (steps.deploy-existing.outcome == 'success' || steps.deploy-existing.outcome == 'skipped') && (steps.deploy-new.outcome == 'success' || steps.deploy-new.outcome == 'skipped')
578604
continue-on-error: true
579605
run: |
580606
# Use auto-detected critical stacks if enabled, otherwise use manual input
@@ -596,7 +622,7 @@ jobs:
596622
597623
- name: Cleanup unused images
598624
id: cleanup
599-
if: steps.backup.outputs.deployment_needed == 'true' && steps.deploy.outcome == 'success' && steps.health.outcome == 'success'
625+
if: steps.backup.outputs.deployment_needed == 'true' && (steps.deploy-existing.outcome == 'success' || steps.deploy-existing.outcome == 'skipped') && (steps.deploy-new.outcome == 'success' || steps.deploy-new.outcome == 'skipped') && steps.health.outcome == 'success'
600626
continue-on-error: true
601627
run: |
602628
echo "::group::Cleaning up unused Docker images"
@@ -609,7 +635,7 @@ jobs:
609635
610636
- name: Rollback Dockge
611637
id: rollback-dockge
612-
if: steps.backup.outputs.deployment_needed == 'true' && (steps.deploy.outcome == 'failure' || steps.health.outcome == 'failure') && inputs.has-dockge == true
638+
if: steps.backup.outputs.deployment_needed == 'true' && (steps.deploy-existing.outcome == 'failure' || steps.deploy-new.outcome == 'failure' || steps.health.outcome == 'failure') && inputs.has-dockge == true
613639
continue-on-error: true
614640
run: |
615641
echo "🔄 Rolling back Dockge to previous version..."
@@ -623,7 +649,7 @@ jobs:
623649
624650
- name: Rollback to Previous Version
625651
id: rollback
626-
if: steps.backup.outputs.deployment_needed == 'true' && (steps.deploy.outcome == 'failure' || steps.health.outcome == 'failure') && (inputs.has-dockge == false || steps.rollback-dockge.outcome == 'success')
652+
if: steps.backup.outputs.deployment_needed == 'true' && (steps.deploy-existing.outcome == 'failure' || steps.deploy-new.outcome == 'failure' || steps.health.outcome == 'failure') && (inputs.has-dockge == false || steps.rollback-dockge.outcome == 'success')
627653
continue-on-error: true
628654
run: |
629655
echo "🔄 **INITIATING ROLLBACK**"
@@ -657,7 +683,7 @@ jobs:
657683
# visibility into what services are running, which is critical for incident response
658684
- name: Verify Rollback Health
659685
id: rollback-health
660-
if: steps.backup.outputs.deployment_needed == 'true' && (steps.deploy.outcome == 'failure' || steps.health.outcome == 'failure') && steps.rollback.conclusion != 'skipped'
686+
if: steps.backup.outputs.deployment_needed == 'true' && (steps.deploy-existing.outcome == 'failure' || steps.deploy-new.outcome == 'failure' || steps.health.outcome == 'failure') && steps.rollback.conclusion != 'skipped'
661687
continue-on-error: true
662688
run: |
663689
echo "🔍 Verifying rollback health status"
@@ -763,15 +789,15 @@ jobs:
763789
echo "✅ Repository already at target commit"
764790
echo "📋 Target stacks: $STACK_LIST"
765791
echo "🔄 SHA: ${{ inputs.target-ref }}"
766-
elif [ "${{ inputs.force-deploy }}" = "true" ] && [ "${{ steps.deploy.outcome }}" == "success" ] && [ "${{ steps.health.outcome }}" == "success" ]; then
792+
elif [ "${{ inputs.force-deploy }}" = "true" ] && ([ "${{ steps.deploy-existing.outcome }}" == "success" ] || [ "${{ steps.deploy-existing.outcome }}" == "skipped" ]) && ([ "${{ steps.deploy-new.outcome }}" == "success" ] || [ "${{ steps.deploy-new.outcome }}" == "skipped" ]) && [ "${{ steps.health.outcome }}" == "success" ]; then
767793
echo "🔄 **FORCE DEPLOYMENT SUCCESSFUL**"
768794
echo "✅ All stacks force-deployed and healthy"
769795
echo "📋 Deployed stacks: $STACK_LIST"
770796
echo "🔄 SHA: ${{ inputs.target-ref }}"
771797
if [ "${{ steps.cleanup.outcome }}" == "success" ]; then
772798
echo "🧹 Cleanup completed successfully"
773799
fi
774-
elif [ "${{ steps.deploy.outcome }}" == "success" ] && [ "${{ steps.health.outcome }}" == "success" ]; then
800+
elif ([ "${{ steps.deploy-existing.outcome }}" == "success" ] || [ "${{ steps.deploy-existing.outcome }}" == "skipped" ]) && ([ "${{ steps.deploy-new.outcome }}" == "success" ] || [ "${{ steps.deploy-new.outcome }}" == "skipped" ]) && [ "${{ steps.health.outcome }}" == "success" ]; then
775801
echo "🎉 **DEPLOYMENT SUCCESSFUL**"
776802
echo "✅ All stacks deployed and healthy"
777803
echo "📋 Deployed stacks: $STACK_LIST"
@@ -781,7 +807,8 @@ jobs:
781807
fi
782808
else
783809
echo "💥 **DEPLOYMENT FAILED**"
784-
echo "❌ Deploy status: ${{ steps.deploy.outcome }}"
810+
echo "❌ Deploy existing status: ${{ steps.deploy-existing.outcome }}"
811+
echo "❌ Deploy new status: ${{ steps.deploy-new.outcome }}"
785812
echo "❌ Health check status: ${{ steps.health.outcome }}"
786813
if [ "${{ steps.rollback.outcome }}" == "success" ]; then
787814
echo "🔄 Rollback completed successfully"

0 commit comments

Comments
 (0)