diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6fc1f56b1bc..95e368c7e85 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,7 +3,7 @@ name: CD - Deploy - API on: workflow_dispatch: inputs: - fcc_api_log_level: + api_log_lvl: description: 'Log level for the API' type: choice options: @@ -11,54 +11,74 @@ on: - info - warn default: info + workflow_run: + workflows: [CI - Node.js] + types: + - completed + branches: + - prod-* jobs: - static: - name: Set Static Data + setup-jobs: + name: Setup Jobs runs-on: ubuntu-22.04 outputs: - site_tld: ${{ steps.static_data.outputs.site_tld }} - environment: ${{ steps.static_data.outputs.environment }} - fcc_api_log_level: ${{ steps.static_data.outputs.fcc_api_log_level }} + site_tld: ${{ steps.setup.outputs.site_tld }} + tgt_env_short: ${{ steps.setup.outputs.tgt_env_short }} + tgt_env_long: ${{ steps.setup.outputs.tgt_env_long }} + api_log_lvl: ${{ steps.setup.outputs.api_log_lvl }} steps: - - name: Set Static Data - id: static_data + - name: Setup + id: setup run: | - if [ "${{ github.ref }}" == "refs/heads/prod-staging" ]; then - echo "site_tld=dev" >> $GITHUB_OUTPUT - echo "environment=stg" >> $GITHUB_OUTPUT - echo "fcc_api_log_level=${{ inputs.fcc_api_log_level || 'info' }}" >> $GITHUB_OUTPUT - elif [ "${{ github.ref }}" == "refs/heads/prod-current" ]; then - echo "site_tld=org" >> $GITHUB_OUTPUT - echo "environment=prd" >> $GITHUB_OUTPUT - echo "fcc_api_log_level=${{ inputs.fcc_api_log_level || 'info' }}" >> $GITHUB_OUTPUT - else - echo "site_tld=dev" >> $GITHUB_OUTPUT - echo "environment=stg" >> $GITHUB_OUTPUT - echo "fcc_api_log_level=${{ inputs.fcc_api_log_level || 'info' }}" >> $GITHUB_OUTPUT + if [[ "${{ github.event_name }}" == "workflow_run" && "${{ github.event.workflow_run.conclusion }}" != "success" ]]; then + echo "Node.js tests failed in the triggering workflow run. Check logs in its workflow run. Exiting." + exit 1 fi + if [[ "${{ github.event_name }}" == "workflow_run" ]]; then + BRANCH="${{ github.event.workflow_run.head_branch }}" + else + BRANCH="${{ github.ref_name }}" + fi + + echo "Current branch: $BRANCH" + + case "$BRANCH" in + "prod-current") + echo "site_tld=org" >> $GITHUB_OUTPUT + echo "tgt_env_short=prd" >> $GITHUB_OUTPUT + echo "tgt_env_long=production" >> $GITHUB_OUTPUT + echo "api_log_lvl=${{ inputs.api_log_lvl || 'info' }}" >> $GITHUB_OUTPUT + ;; + *) + echo "site_tld=dev" >> $GITHUB_OUTPUT + echo "tgt_env_short=stg" >> $GITHUB_OUTPUT + echo "tgt_env_long=staging" >> $GITHUB_OUTPUT + echo "api_log_lvl=${{ inputs.api_log_lvl || 'info' }}" >> $GITHUB_OUTPUT + ;; + esac + build: name: Build & Push - needs: static + needs: setup-jobs uses: ./.github/workflows/docker-docr.yml with: - site_tld: ${{ needs.static.outputs.site_tld }} + site_tld: ${{ needs.setup-jobs.outputs.site_tld }} app: api secrets: inherit deploy: - name: Deploy to Docker Swarm -- ${{ needs.static.outputs.environment }} + name: Deploy to Docker Swarm -- ${{ needs.setup-jobs.outputs.tgt_env_short }} runs-on: ubuntu-22.04 - needs: [static, build] + needs: [setup-jobs, build] env: TS_USERNAME: ${{ secrets.TS_USERNAME }} TS_MACHINE_NAME: ${{ secrets.TS_MACHINE_NAME }} permissions: deployments: write environment: - name: ${{ needs.static.outputs.environment }} - url: https://api.freecodecamp.${{ needs.static.outputs.site_tld }}/status/ping?version=${{ needs.build.outputs.tagname }} + name: ${{ needs.setup-jobs.outputs.tgt_env_short }}-api steps: - name: Setup and connect to Tailscale network @@ -66,7 +86,7 @@ jobs: with: oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} - hostname: gha-${{needs.static.outputs.environment}}-api-ci-${{ github.run_id }} + hostname: gha-${{needs.setup-jobs.outputs.tgt_env_short}}-api-ci-${{ github.run_id }} tags: tag:ci version: latest @@ -86,47 +106,97 @@ jobs: - name: Deploy with Docker Stack env: - # These are set in the "Environment" specifc secrets AGE_ENCRYPTED_ASC_SECRETS: ${{ secrets.AGE_ENCRYPTED_ASC_SECRETS }} AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }} + # These are set in the "Environment" specifc secrets + # DOCKER_REGISTRY + # MONGOHQ_URL + # SENTRY_DSN + # SENTRY_ENVIRONMENT + # AUTH0_CLIENT_ID + # AUTH0_CLIENT_SECRET + # AUTH0_DOMAIN + # JWT_SECRET + # COOKIE_SECRET + # COOKIE_DOMAIN + # SES_ID + # SES_SECRET + # GROWTHBOOK_FASTIFY_API_HOST + # GROWTHBOOK_FASTIFY_CLIENT_KEY + # HOME_LOCATION + # API_LOCATION + # STRIPE_SECRET_KEY # These are set in the static job above - STACK_NAME: ${{ needs.static.outputs.environment }}-api + STACK_NAME: ${{ needs.setup-jobs.outputs.tgt_env_short }}-api DEPLOYMENT_VERSION: ${{ needs.build.outputs.tagname }} - FCC_API_LOG_LEVEL: ${{ needs.static.outputs.fcc_api_log_level }} + DEPLOYMENT_ENV: ${{ needs.setup-jobs.outputs.site_tld }} + FCC_API_LOG_LEVEL: ${{ needs.setup-jobs.outputs.api_log_lvl }} run: | REMOTE_SCRIPT=" - set -e - echo -e '\nLOG:Deploying API to \$TS_MACHINE_NAME...' - cd /home/\${TS_USERNAME}/docker-swarm-config/stacks/api || { echo \"Error: Failed to change directory\"; exit 1; } - which age > /dev/null || { echo \"Error: age not installed\"; exit 1; } + set -e + echo -e '\nLOG:Deploying API to $TS_MACHINE_NAME...' + cd /home/$TS_USERNAME/docker-swarm-config/stacks/api || { echo \"Error: Failed to change directory\"; exit 1; } + which age > /dev/null || { echo \"Error: age not installed\"; exit 1; } - echo -e '\nLOG:Decrypting secrets...' - echo \"\${AGE_ENCRYPTED_ASC_SECRETS}\" > secrets.age.asc - echo \"\${AGE_SECRET_KEY}\" > age.key && chmod 600 age.key - age --identity age.key --decrypt secrets.age.asc > .env - rm -f age.key secrets.age.asc + echo -e '\nLOG:Decrypting secrets...' + echo \"$AGE_ENCRYPTED_ASC_SECRETS\" > secrets.age.asc + echo \"$AGE_SECRET_KEY\" > age.key && chmod 600 age.key + age --identity age.key --decrypt secrets.age.asc > .env + rm -f age.key secrets.age.asc - echo -e '\nLOG:Adding deployment variables...' - { - echo \"DEPLOYMENT_VERSION=\${DEPLOYMENT_VERSION}\" - echo \"FCC_API_LOG_LEVEL=\${FCC_API_LOG_LEVEL}\" - } >> .env + echo -e '\nLOG:Cleaning up .env file...' + touch .env.tmp + while IFS= read -r line; do + if [[ \$line =~ ^[A-Za-z0-9_]+=.*$ ]]; then + # Extract the key (part before the first =) + key=\${line%%=*} + # Remove any previous line with this key + sed -i \"/^\${key}=/d\" .env.tmp + fi + # Append the current line + echo \"\$line\" >> .env.tmp + done < .env + mv .env.tmp .env - echo -e '\nLOG:Exporting environment variables...' - while IFS='=' read -r key value; do - if [[ -n \$key && ! \$key =~ ^# ]]; then - export \"\${key}=\${value}\" + echo -e '\nLOG:Adding deployment variables...' + { + echo \"DEPLOYMENT_VERSION=$DEPLOYMENT_VERSION\" + echo \"DEPLOYMENT_ENV=$DEPLOYMENT_ENV\" + echo \"FCC_API_LOG_LEVEL=$FCC_API_LOG_LEVEL\" + } >> .env + + echo -e '\nLOG:Sourcing environment...' + while IFS='=' read -r key value; do + if [[ -n \"\$key\" && ! \"\$key\" =~ ^# ]]; then + export \"\${key}=\${value}\" + fi + done < .env + rm -rf .env + + echo -e '\nLOG:Validating environment...' + if [[ -z \"\$DOCKER_REGISTRY\" || -z \"\$DEPLOYMENT_ENV\" || -z \"\$DEPLOYMENT_VERSION\" || -z \"\$MONGOHQ_URL\" || -z \"\$FCC_API_LOG_LEVEL\" ]]; then + echo \"Error: Missing required environment variables\" + exit 1 fi - done < .env - rm -f .env + if [[ \"\$DEPLOYMENT_VERSION\" != \"$DEPLOYMENT_VERSION\" ]]; then + echo \"Error: Version mismatch. Expected: $DEPLOYMENT_VERSION, Got: \$DEPLOYMENT_VERSION\" + exit 1 + fi + env | grep -E 'DOMAIN|DEPLOYMENT' || { echo \"Error: Required environment variables not found\"; exit 1; } - echo -e '\nLOG:Validating environment and config...' - env | grep -E 'DOMAIN|DEPLOYMENT' || { echo \"Error: Required environment variables not found\"; exit 1; } - docker stack config -c stack-api.yml > /dev/null || { echo \"Error: Invalid stack configuration\"; exit 1; } + echo -e '\nLOG:Checking stack configuration...' + CONFIG_OUTPUT="/dev/null" + if [[ \"\$FCC_API_LOG_LEVEL\" == \"debug\" ]]; then + CONFIG_FILENAME="debug-docker-stack-config-\${DEPLOYMENT_VERSION}.yml" + echo -e '\nLOG:Saving stack configuration to $CONFIG_FILENAME for debugging...' + CONFIG_OUTPUT="\$CONFIG_FILENAME" + fi + docker stack config -c stack-api.yml > "$CONFIG_OUTPUT" || { echo \"Error: Invalid stack configuration\"; exit 1; } - echo -e '\nLOG:Deploying stack...' - docker stack deploy -c stack-api.yml --prune --with-registry-auth --detach=false \${STACK_NAME} - echo -e '\nLOG:Finished deployment.' + echo -e '\nLOG:Deploying stack...' + docker stack deploy -c stack-api.yml --prune --with-registry-auth --detach=false $STACK_NAME + + echo -e '\nLOG:Finished deployment.' " MACHINE_IP=$(tailscale ip -4 $TS_MACHINE_NAME) ssh $TS_USERNAME@$MACHINE_IP "$REMOTE_SCRIPT"