diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..6cee824 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,321 @@ +name: CD + +on: + push: + branches: + - main + +jobs: + detect-changes: + runs-on: ubuntu-latest + outputs: + deploy-static: ${{ steps.filter.outputs.static }} + deploy-ts-web: ${{ steps.filter.outputs.ts-web }} + deploy-py: ${{ steps.filter.outputs.py }} + steps: + - uses: actions/checkout@v4 + + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + static: + - 'static/**' + - 'bin/kamal' + - 'bin/iac' + - 'config/deploy/static.yml' + - '.github/workflows/cd.yml' + ts-web: + - 'ts/**' + - 'bin/kamal' + - 'bin/iac' + - 'config/deploy/ts-web.yml' + - '.github/workflows/cd.yml' + py: + - 'py/**' + - 'bin/kamal' + - 'bin/iac' + - 'config/deploy/py.yml' + - '.github/workflows/cd.yml' + + deploy-static: + needs: detect-changes + if: needs.detect-changes.outputs.deploy-static == 'true' + runs-on: ubuntu-latest + environment: production + env: + DOCKER_BUILDKIT: 1 + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + CI: true + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2.2 + + - name: Install Kamal + run: gem install kamal --version 2.4.0 + + - name: Install 1Password CLI + uses: 1password/install-cli-action@v1 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_wrapper: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Verify 1Password Access + run: | + source bin/vault + echo "Testing 1Password access..." + op whoami + + - name: Initialize Terraform + run: bin/iac production init -input=false + + - name: Setup SSH Agent + run: | + # Create .kamal directory for any temporary files + mkdir -p .kamal + chmod 700 .kamal + + # Get SSH key from Terraform + echo "Getting SSH key from Terraform..." + SSH_KEY=$(bin/iac production output -raw ssh_private_key 2>&1) + + # Debug output + echo "Output lines: $(echo "$SSH_KEY" | wc -l)" + + # Verify we got a key + if [ -z "$SSH_KEY" ] || ! echo "$SSH_KEY" | grep -q "BEGIN.*PRIVATE KEY"; then + echo "Error: Failed to retrieve valid SSH key from Terraform" + echo "Got: $SSH_KEY" + exit 1 + fi + + echo "Successfully retrieved SSH key" + + # Start SSH agent + eval $(ssh-agent -s) + + # Add key to agent + echo "Adding SSH key to agent..." + ssh-add - <<< "$SSH_KEY" + + # Make agent available to subsequent steps + echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> $GITHUB_ENV + echo "SSH_AGENT_PID=$SSH_AGENT_PID" >> $GITHUB_ENV + + # Verify key was added + echo "Keys in agent:" + ssh-add -l + + - name: Setup SSH config + run: | + mkdir -p ~/.ssh + chmod 700 ~/.ssh + cat >> ~/.ssh/config <&1) + + # Debug output + echo "Output lines: $(echo "$SSH_KEY" | wc -l)" + + # Verify we got a key + if [ -z "$SSH_KEY" ] || ! echo "$SSH_KEY" | grep -q "BEGIN.*PRIVATE KEY"; then + echo "Error: Failed to retrieve valid SSH key from Terraform" + echo "Got: $SSH_KEY" + exit 1 + fi + + echo "Successfully retrieved SSH key" + + # Start SSH agent + eval $(ssh-agent -s) + + # Add key to agent + echo "Adding SSH key to agent..." + ssh-add - <<< "$SSH_KEY" + + # Make agent available to subsequent steps + echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> $GITHUB_ENV + echo "SSH_AGENT_PID=$SSH_AGENT_PID" >> $GITHUB_ENV + + # Verify key was added + echo "Keys in agent:" + ssh-add -l + + - name: Setup SSH config + run: | + mkdir -p ~/.ssh + chmod 700 ~/.ssh + cat >> ~/.ssh/config <&1) + + # Debug output + echo "Output lines: $(echo "$SSH_KEY" | wc -l)" + + # Verify we got a key + if [ -z "$SSH_KEY" ] || ! echo "$SSH_KEY" | grep -q "BEGIN.*PRIVATE KEY"; then + echo "Error: Failed to retrieve valid SSH key from Terraform" + echo "Got: $SSH_KEY" + exit 1 + fi + + echo "Successfully retrieved SSH key" + + # Start SSH agent + eval $(ssh-agent -s) + + # Add key to agent + echo "Adding SSH key to agent..." + ssh-add - <<< "$SSH_KEY" + + # Make agent available to subsequent steps + echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> $GITHUB_ENV + echo "SSH_AGENT_PID=$SSH_AGENT_PID" >> $GITHUB_ENV + + # Verify key was added + echo "Keys in agent:" + ssh-add -l + + - name: Setup SSH config + run: | + mkdir -p ~/.ssh + chmod 700 ~/.ssh + cat >> ~/.ssh/config </dev export SSH_PRIVATE_KEY=$("$PROJECT_ROOT/bin/iac" "$STAGE" output -raw ssh_private_key 2>/dev/null | tail -n -13 || echo "") export DOCKER_HUB_USERNAME=$("$PROJECT_ROOT/bin/vault" read DOCKER_HUB_USERNAME) export PROJECT_NAME=$PROJECT_NAME -# write the ssh private key to a temp file with proper permissions -SSH_PRIVATE_KEY_FILE=$(mktemp) -echo "$SSH_PRIVATE_KEY" > "$SSH_PRIVATE_KEY_FILE" -chmod 600 "$SSH_PRIVATE_KEY_FILE" -export SSH_PRIVATE_KEY_FILE -# make it so that the temp file is *ALWAYS* removed even if the script exits abnormally -trap 'print_info "Cleaning up SSH private key file" && rm "$SSH_PRIVATE_KEY_FILE"' EXIT +# Only write SSH key file if not in CI +if [[ -z "${CI:-}" ]]; then + # Local: Use key file + SSH_PRIVATE_KEY_FILE="$PROJECT_ROOT/.kamal/ssh_key" + echo "$SSH_PRIVATE_KEY" > "$SSH_PRIVATE_KEY_FILE" + chmod 600 "$SSH_PRIVATE_KEY_FILE" + export SSH_PRIVATE_KEY_FILE + trap 'print_info "Cleaning up SSH private key file" && rm -f "$SSH_PRIVATE_KEY_FILE"' EXIT +else + # CI: SSH agent is already set up, don't export SSH_PRIVATE_KEY_FILE + print_info "Running in CI - using SSH agent for authentication" +fi # Determine the hostname for the service given the service name and stage HOST_NAME=$(get_service_hostname "$SERVICE" "$STAGE") diff --git a/config/deploy/py.yml b/config/deploy/py.yml index 4b777fa..0a7b5a1 100644 --- a/config/deploy/py.yml +++ b/config/deploy/py.yml @@ -1,37 +1,25 @@ service: <%= ENV['PROJECT_NAME'] %>-py -# NOTE (amiller68): do not change this! -# It is important that this is kept in sync with -# the name of the service as described in the file -# name. -# i.e. if the file name is py.yml, then the service name must be <%= ENV['PROJECT_NAME'] %>-py -# Container image using Docker Hub image: <%= ENV['DOCKER_HUB_USERNAME'] %>/<%= ENV['PROJECT_NAME'] %>-py -# Deploy to these servers servers: web: - "<%= ENV['SERVER_IP'].strip %>" -# SSH configuration ssh: user: root keys: ["<%= ENV['SSH_PRIVATE_KEY_FILE'] %>"] - keys_only: true -# Registry configuration for Docker Hub registry: username: <%= ENV['DOCKER_HUB_USERNAME'] %> password: - DOCKER_HUB_TOKEN -# Builder configuration builder: dockerfile: py/Dockerfile context: "." arch: amd64 -# Proxy configuration for SSL proxy: ssl: true host: <%= ENV['HOST_NAME'] %> @@ -51,7 +39,6 @@ env: - GOOGLE_O_AUTH_CLIENT_ID - GOOGLE_O_AUTH_CLIENT_SECRET -# Accessories (PostgreSQL database - internal only) accessories: postgres: image: postgres:17-alpine diff --git a/config/deploy/static.yml b/config/deploy/static.yml index f9a8c79..4b0e4a2 100644 --- a/config/deploy/static.yml +++ b/config/deploy/static.yml @@ -14,7 +14,6 @@ servers: ssh: user: root keys: ["<%= ENV['SSH_PRIVATE_KEY_FILE'] %>"] - keys_only: true # Registry configuration for Docker Hub registry: diff --git a/config/deploy/ts-web.yml b/config/deploy/ts-web.yml index 4f4189c..d1d6140 100644 --- a/config/deploy/ts-web.yml +++ b/config/deploy/ts-web.yml @@ -17,7 +17,6 @@ servers: ssh: user: root keys: ["<%= ENV['SSH_PRIVATE_KEY_FILE'] %>"] - keys_only: true # Registry configuration for Docker Hub registry: diff --git a/config/example.yml b/config/example.yml index b94bea5..25ea9df 100644 --- a/config/example.yml +++ b/config/example.yml @@ -14,7 +14,6 @@ servers: ssh: user: root keys: ["<%= ENV['SSH_PRIVATE_KEY_FILE'] %>"] - keys_only: true # Registry configuration for Docker Hub registry: