1- name : CD -- Deploy - API (Docker Swarm)
1+ name : CD -- Deploy - API
22
33on :
44 workflow_dispatch :
5+ inputs :
6+ fcc_api_log_level :
7+ description : ' Log level for the API'
8+ type : choice
9+ options :
10+ - debug
11+ - info
12+ - warn
13+ default : info
514
615jobs :
716 static :
17+ name : Set Static Data
818 runs-on : ubuntu-24.04
919 outputs :
1020 site_tld : ${{ steps.static_data.outputs.site_tld }}
11- environment_long : ${{ steps.static_data.outputs.environment_long }}
12- environment_short : ${{ steps.static_data.outputs.environment_short }}
13-
21+ environment : ${{ steps.static_data.outputs.environment }}
22+ fcc_api_log_level : ${{ steps.static_data.outputs.fcc_api_log_level }}
1423 steps :
15- - name : Set site_tld
24+ - name : Set Static Data
1625 id : static_data
1726 run : |
1827 if [ "${{ github.ref }}" == "refs/heads/prod-staging" ]; then
1928 echo "site_tld=dev" >> $GITHUB_OUTPUT
20- echo "environment_long=staging " >> $GITHUB_OUTPUT
21- echo "environment_short=stg " >> $GITHUB_OUTPUT
29+ echo "environment=stg " >> $GITHUB_OUTPUT
30+ echo "fcc_api_log_level=${{ inputs.fcc_api_log_level || 'info' }} " >> $GITHUB_OUTPUT
2231 elif [ "${{ github.ref }}" == "refs/heads/prod-current" ]; then
2332 echo "site_tld=org" >> $GITHUB_OUTPUT
24- echo "environment_long=production " >> $GITHUB_OUTPUT
25- echo "environment_short=prd " >> $GITHUB_OUTPUT
33+ echo "environment=prd " >> $GITHUB_OUTPUT
34+ echo "fcc_api_log_level=${{ inputs.fcc_api_log_level || 'info' }} " >> $GITHUB_OUTPUT
2635 else
2736 echo "site_tld=dev" >> $GITHUB_OUTPUT
28- echo "environment_long=staging " >> $GITHUB_OUTPUT
29- echo "environment_short=stg " >> $GITHUB_OUTPUT
37+ echo "environment=stg " >> $GITHUB_OUTPUT
38+ echo "fcc_api_log_level=${{ inputs.fcc_api_log_level || 'info' }} " >> $GITHUB_OUTPUT
3039 fi
3140
3241 build :
33- name : Build & Push Docker Image
42+ name : Build & Push
3443 needs : static
3544 uses : ./.github/workflows/docker-docr.yml
3645 with :
3746 site_tld : ${{ needs.static.outputs.site_tld }}
3847 app : api
48+ secrets : inherit
3949
4050 deploy :
51+ name : Deploy to Docker Swarm -- ${{ needs.static.outputs.environment }}
4152 runs-on : ubuntu-24.04
4253 needs : [static, build]
54+ env :
55+ TS_USERNAME : ${{ secrets.TS_USERNAME }}
56+ TS_MACHINE_NAME : ${{ secrets.TS_MACHINE_NAME }}
4357 permissions :
4458 deployments : write
4559 environment :
46- name : ${{ needs.static.outputs.environment_long }}
60+ name : ${{ needs.static.outputs.environment }}
4761 url : https://api.freecodecamp.${{ needs.static.outputs.site_tld }}/status/ping?version=${{ needs.build.outputs.tagname }}
4862
4963 steps :
@@ -66,73 +80,49 @@ jobs:
6680
6781 - name : Check connection
6882 run : |
69- tailscale status | grep -q "$TS_MACHINE_NAME" || { echo "Machine not found"; exit 1; }
83+ tailscale status | grep -q "$TS_MACHINE_NAME" || { echo "Error: Machine not found"; exit 1; }
7084 ssh $TS_USERNAME@$TS_MACHINE_NAME "uptime"
7185
7286 - name : Deploy with Docker Stack
7387 env :
74- # These are set in the "Environment" secrets
75- API_LOCATION : ${{ secrets.API_LOCATION }}
76- AUTH0_CLIENT_ID : ${{ secrets.AUTH0_CLIENT_ID }}
77- AUTH0_CLIENT_SECRET : ${{ secrets.AUTH0_CLIENT_SECRET }}
78- AUTH0_DOMAIN : ${{ secrets.AUTH0_DOMAIN }}
79- COOKIE_DOMAIN : ${{ secrets.COOKIE_DOMAIN }}
80- COOKIE_SECRET : ${{ secrets.COOKIE_SECRET }}
81- DOCKER_REGISTRY : ${{ secrets.DOCKER_REGISTRY }}
82- FCC_API_LOG_LEVEL : ${{ secrets.FCC_API_LOG_LEVEL }}
83- GROWTHBOOK_FASTIFY_API_HOST : ${{ secrets.GROWTHBOOK_FASTIFY_API_HOST }}
84- GROWTHBOOK_FASTIFY_CLIENT_KEY : ${{ secrets.GROWTHBOOK_FASTIFY_CLIENT_KEY }}
85- HOME_LOCATION : ${{ secrets.HOME_LOCATION }}
86- JWT_SECRET : ${{ secrets.JWT_SECRET }}
87- MONGOHQ_URL : ${{ secrets.MONGOHQ_URL }}
88- SENTRY_DSN : ${{ secrets.SENTRY_DSN }}
89- SES_ID : ${{ secrets.SES_ID }}
90- SES_SECRET : ${{ secrets.SES_SECRET }}
91- STRIPE_SECRET_KEY : ${{ secrets.STRIPE_SECRET_KEY }}
88+ # These are set in the "Environment" specifc secrets
89+ AGE_ENCRYPTED_ASC_SECRETS : ${{ secrets.AGE_ENCRYPTED_ASC_SECRETS }}
90+ AGE_SECRET_KEY : ${{ secrets.AGE_SECRET_KEY }}
9291 # These are set in the static job above
93- DEPLOYMENT_ENV : ${{ needs.static.outputs.site_tld }}
92+ STACK_NAME : ${{ needs.static.outputs.environment }}-api
9493 DEPLOYMENT_VERSION : ${{ needs.build.outputs.tagname }}
95- SENTRY_ENVIRONMENT : api-${{ needs.static.outputs.site_tld }}
96- STACK_NAME : ${{ needs.static.outputs.environment_short }}-api
94+ FCC_API_LOG_LEVEL : ${{ needs.static.outputs.fcc_api_log_level }}
9795 run : |
9896 ssh $TS_USERNAME@$TS_MACHINE_NAME /bin/bash << EOF
97+ set -e
98+ cd /home/${TS_USERNAME}/docker-swarm-config/stacks/api || { echo "Error: Failed to change directory"; exit 1; }
99+ which age > /dev/null || { echo "Error: age not installed"; exit 1; }
99100
100- cd /home/${TS_USERNAME}/docker-swarm-config/stacks/api || { echo "Failed to change directory"; exit 1; }
101- echo "Debug: Current directory: \$(pwd)"
102-
103- echo "API_LOCATION=${API_LOCATION}" >> .env.tmp
104- echo "AUTH0_CLIENT_ID=${AUTH0_CLIENT_ID}" >> .env.tmp
105- echo "AUTH0_CLIENT_SECRET=${AUTH0_CLIENT_SECRET}" >> .env.tmp
106- echo "AUTH0_DOMAIN=${AUTH0_DOMAIN}" >> .env.tmp
107- echo "COOKIE_DOMAIN=${COOKIE_DOMAIN}" >> .env.tmp
108- echo "COOKIE_SECRET=${COOKIE_SECRET}" >> .env.tmp
109- echo "DOCKER_REGISTRY=${DOCKER_REGISTRY}" >> .env.tmp
110- echo "FCC_API_LOG_LEVEL=${FCC_API_LOG_LEVEL}" >> .env.tmp
111- echo "GROWTHBOOK_FASTIFY_API_HOST=${GROWTHBOOK_FASTIFY_API_HOST}" >> .env.tmp
112- echo "GROWTHBOOK_FASTIFY_CLIENT_KEY=${GROWTHBOOK_FASTIFY_CLIENT_KEY}" >> .env.tmp
113- echo "HOME_LOCATION=${HOME_LOCATION}" >> .env.tmp
114- echo "JWT_SECRET=${JWT_SECRET}" >> .env.tmp
115- echo "MONGOHQ_URL=${MONGOHQ_URL}" >> .env.tmp
116- echo "SENTRY_DSN=${SENTRY_DSN}" >> .env.tmp
117- echo "SES_ID=${SES_ID}" >> .env.tmp
118- echo "SES_SECRET=${SES_SECRET}" >> .env.tmp
119- echo "STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}" >> .env.tmp
101+ # Decrypt secrets
102+ echo "${AGE_ENCRYPTED_ASC_SECRETS}" > secrets.age.asc
103+ echo "${AGE_SECRET_KEY}" > age.key && chmod 600 age.key
104+ age --identity age.key --decrypt secrets.age.asc > .env
105+ rm -f age.key secrets.age.asc
120106
121- echo "DEPLOYMENT_ENV=${DEPLOYMENT_ENV}" >> .env.tmp
122- echo "DEPLOYMENT_VERSION=${DEPLOYMENT_VERSION}" >> .env.tmp
123- echo "SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT}" >> .env.tmp
107+ # Add deployment variables
108+ {
109+ echo "DEPLOYMENT_VERSION=${DEPLOYMENT_VERSION}"
110+ echo "FCC_API_LOG_LEVEL=${FCC_API_LOG_LEVEL}"
111+ } >> .env
124112
125- set -a
126- source .env.tmp || { echo "Failed to source .env.tmp"; exit 1; }
127- set +a
128- rm -f .env.tmp
113+ # Export environment variables with proper escaping
114+ while IFS='=' read -r key value; do
115+ if [[ -n \$key && ! \$key =~ ^# ]]; then
116+ export "\${key}=\${value}"
117+ fi
118+ done < .env
119+ rm -f .env
129120
130- echo "Debug: Sanity check variables: "
131- env | grep -E '^DEPLOYMENT' || { echo 'Vars not found'; exit 1; }
132- echo "Debug: Sanity check config: "
133- docker stack config -c stack-api.yml | rg 'DOMAIN' || { echo 'Invalid stack config'; exit 1; }
121+ # Validate environment and config
122+ env | grep -E 'DOMAIN|DEPLOYMENT' || { echo "Error: Required environment variables not found"; exit 1; }
123+ docker stack config -c stack-api.yml > /dev/null || { echo "Error: Invalid stack configuration"; exit 1; }
134124
135- # Dump the stack config to a file for debugging, this will be replaced with a deploy command
136- docker stack config -c stack-api.yml > /tmp/stack-api-${DEPLOYMENT_VERSION}.yml
125+ # Deploy
126+ docker stack deploy -c stack-api.yml --prune --with-registry-auth --detach=false ${STACK_NAME}
137127 EOF
138128 shell : bash
0 commit comments