name: Build & Deploy (develop) # Runs on: # - Pull requests targeting develop # - Direct pushes to develop # main is intentionally NOT included yet (no prod target configured) on: push: branches: - develop pull_request: branches: - develop jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Resolve image tag id: vars run: | # For PRs, gitea.sha is a temporary merge commit — use the real # PR head commit instead so tags map to actual git history. FULL_SHA="${{ gitea.event.pull_request.head.sha || gitea.sha }}" SHORT_SHA="${FULL_SHA:0:7}" BUILD_TIME=$(date -u +%Y%m%d-%H%M%S) echo "sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT" echo "tag=${BUILD_TIME}-${SHORT_SHA}" >> "$GITHUB_OUTPUT" - name: Build Docker image (tagged by UTC build time and commit SHA) run: | docker build -t robot-market:${{ steps.vars.outputs.tag }} . - name: Save previous tag for rollback id: backup run: | if [ -f /opt/robot-market-front/.env ]; then cp /opt/robot-market-front/.env /opt/robot-market-front/.env.previous echo "available=true" >> "$GITHUB_OUTPUT" fi - name: Point compose at this build run: | echo "IMAGE_TAG=${{ steps.vars.outputs.tag }}" > /opt/robot-market-front/.env - name: Deploy via compose id: deploy run: | docker compose --env-file /opt/robot-market-front/.env \ -f /opt/robot-market-front/docker-compose.yml \ up -d --force-recreate - name: Health check (retries for ~30s) id: health run: | for i in $(seq 1 15); do if docker exec robot-market node -e " require('http').get('http://localhost:3000', res => { process.exit(res.statusCode < 400 ? 0 : 1); }).on('error', () => process.exit(1)); " 2>/dev/null; then echo "Container is healthy" exit 0 fi echo "Not ready yet, retrying..." sleep 2 done echo "Health check failed after 30s" exit 1 - name: Roll back after deployment or health check failure if: ${{ failure() && steps.backup.outputs.available == 'true' && (steps.deploy.outcome == 'failure' || steps.health.outcome == 'failure') }} run: | if [ -f /opt/robot-market-front/.env.previous ]; then cp /opt/robot-market-front/.env.previous /opt/robot-market-front/.env docker compose --env-file /opt/robot-market-front/.env \ -f /opt/robot-market-front/docker-compose.yml \ up -d --force-recreate echo "Rolled back to previous image." else echo "No previous deploy recorded — nothing to roll back to." fi - name: Clean up dangling (untagged) layers run: docker image prune -f - name: Prune old build-tagged images (keep last 5) run: | KEEP=5 CURRENT_TAG=$(grep IMAGE_TAG /opt/robot-market-front/.env 2>/dev/null | cut -d= -f2) PREVIOUS_TAG=$(grep IMAGE_TAG /opt/robot-market-front/.env.previous 2>/dev/null | cut -d= -f2) docker images robot-market --format '{{.Tag}} {{.CreatedAt}}' \ | sort -t" " -k2 -r \ | tail -n +$((KEEP + 1)) \ | cut -f1 \ | while read -r tag; do if [ "$tag" != "$CURRENT_TAG" ] && [ "$tag" != "$PREVIOUS_TAG" ] && [ "$tag" != "" ]; then echo "Removing old image robot-market:$tag" docker rmi "robot-market:$tag" 2>/dev/null || true fi done