name: Deploy on: push: tags: - "v*" env: CARGO_TERM_COLOR: always REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} permissions: contents: read packages: write jobs: # ---- CI checks ---- check: name: Check runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master with: toolchain: "1.93.1" components: rustfmt, clippy - name: Cache Cargo artifacts uses: Swatinem/rust-cache@v2 - name: Download MMDB database run: | mkdir -p data curl -fsSL -o data/Merged-IP.mmdb \ "https://github.com/NetworkCats/Merged-IP-Data/releases/latest/download/Merged-IP.mmdb" - name: Check formatting run: cargo fmt --check - name: Run Clippy run: cargo clippy -- -D warnings - name: Run tests run: cargo test # ---- Build and push Docker image ---- build: name: Build Image runs-on: ubuntu-latest needs: check outputs: image_tag: ${{ steps.meta.outputs.tags }} image_full: ${{ steps.full_image.outputs.image }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GHCR uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=semver,pattern={{version}} type=raw,value=latest - name: Build and push image uses: docker/build-push-action@v6 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - name: Set full image reference id: full_image run: | TAG="${GITHUB_REF_NAME#v}" echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${TAG}" >> "$GITHUB_OUTPUT" # ---- Deploy to VPS ---- deploy: name: Deploy runs-on: ubuntu-latest needs: build environment: production concurrency: group: deploy-production cancel-in-progress: false steps: - name: Checkout repository uses: actions/checkout@v4 # ---- Terraform: provision infrastructure ---- - name: Setup Terraform uses: hashicorp/setup-terraform@v3 with: terraform_wrapper: false - name: Terraform init working-directory: deploy/terraform env: AWS_ACCESS_KEY_ID: ${{ secrets.TF_STATE_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.TF_STATE_SECRET_KEY }} run: | terraform init \ -backend-config="bucket=${{ secrets.TF_STATE_BUCKET }}" \ -backend-config="endpoints={s3=\"${{ secrets.TF_STATE_ENDPOINT }}\"}" - name: Terraform apply working-directory: deploy/terraform env: AWS_ACCESS_KEY_ID: ${{ secrets.TF_STATE_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.TF_STATE_SECRET_KEY }} run: | terraform apply -auto-approve \ -var="vultr_api_key=${{ secrets.VULTR_API_KEY }}" \ -var="ssh_public_key=${{ secrets.SSH_PUBLIC_KEY }}" - name: Extract VPS IP (masked) id: vps working-directory: deploy/terraform env: AWS_ACCESS_KEY_ID: ${{ secrets.TF_STATE_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.TF_STATE_SECRET_KEY }} run: | VPS_IP=$(terraform output -raw instance_ip) echo "::add-mask::${VPS_IP}" echo "ip=${VPS_IP}" >> "$GITHUB_OUTPUT" # ---- SSH setup ---- - name: Setup SSH key run: | mkdir -p ~/.ssh echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key for i in 1 2 3; do if ssh-keyscan -H "${{ steps.vps.outputs.ip }}" >> ~/.ssh/known_hosts 2>/dev/null; then echo "ssh-keyscan succeeded on attempt $i" exit 0 fi echo "ssh-keyscan attempt $i failed, retrying in 10s..." sleep 10 done echo "ssh-keyscan failed after 3 attempts" exit 1 # ---- Detect first deployment ---- - name: Detect first deployment id: detect run: | set +e ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \ -i ~/.ssh/deploy_key root@"${{ steps.vps.outputs.ip }}" \ "test -f /opt/rustyip/.active_color" if [ $? -ne 0 ]; then echo "first_deploy=true" >> "$GITHUB_OUTPUT" else echo "first_deploy=false" >> "$GITHUB_OUTPUT" fi # ---- Ansible: configure VPS and deploy ---- - name: Install Ansible run: pip install ansible - name: Generate Ansible inventory run: | cat > deploy/ansible/inventory.yml </dev/null) if [ "$status" = "healthy" ]; then echo "Deployment verified: app-${DEPLOY_COLOR} is healthy" exit 0 fi echo "Waiting for health check... (attempt $i/15)" sleep 4 done echo "Health check failed after 60 seconds" exit 1 # ---- Cleanup ---- - name: Cleanup SSH key if: always() run: rm -f ~/.ssh/deploy_key