chore: import upstream snapshot with attribution
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
name: CD - Deploy - API
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
api_log_lvl:
|
||||
description: 'Log level for the API'
|
||||
type: choice
|
||||
options:
|
||||
- debug
|
||||
- info
|
||||
- warn
|
||||
default: info
|
||||
show_upcoming_changes:
|
||||
description: 'Show upcoming changes (enables upcoming certifications and challenges)'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: deploy-api-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
setup-jobs:
|
||||
name: Setup Jobs
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
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 }}
|
||||
show_upcoming_changes: ${{ steps.setup.outputs.show_upcoming_changes }}
|
||||
steps:
|
||||
- name: Setup
|
||||
id: setup
|
||||
env:
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
SHOW_UPCOMING_CHANGES: ${{ inputs.show_upcoming_changes }}
|
||||
API_LOG_LVL: ${{ inputs.api_log_lvl || 'info' }}
|
||||
run: |
|
||||
echo "Current branch: $BRANCH"
|
||||
|
||||
# Convert boolean input to string 'true' or 'false'
|
||||
if [[ "$SHOW_UPCOMING_CHANGES" == "true" ]]; then
|
||||
echo "show_upcoming_changes=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "show_upcoming_changes=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
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=$API_LOG_LVL" >> "$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=$API_LOG_LVL" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
esac
|
||||
|
||||
build:
|
||||
name: Build & Push
|
||||
needs: setup-jobs
|
||||
uses: ./.github/workflows/docker-docr.yml
|
||||
with:
|
||||
site_tld: ${{ needs.setup-jobs.outputs.site_tld }}
|
||||
app: api
|
||||
show_upcoming_changes: ${{ needs.setup-jobs.outputs.show_upcoming_changes }}
|
||||
secrets:
|
||||
DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||
DOCR_NAME: ${{ secrets.DOCR_NAME }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
deploy:
|
||||
name: Deploy to Docker Swarm -- ${{ needs.setup-jobs.outputs.tgt_env_short }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [setup-jobs, build]
|
||||
env:
|
||||
TS_USERNAME: ${{ secrets.TS_USERNAME }}
|
||||
TS_MACHINE_NAME: ${{ secrets.TS_MACHINE_NAME }}
|
||||
permissions:
|
||||
deployments: write
|
||||
environment:
|
||||
name: ${{ needs.setup-jobs.outputs.tgt_env_short }}-api
|
||||
|
||||
steps:
|
||||
- name: Setup and connect to Tailscale network
|
||||
uses: tailscale/github-action@306e68a486fd2350f2bfc3b19fcd143891a4a2d8 # v4
|
||||
with:
|
||||
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
|
||||
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
|
||||
hostname: gha-${{needs.setup-jobs.outputs.tgt_env_short}}-api-ci-${{ github.run_id }}
|
||||
tags: tag:ci
|
||||
version: latest
|
||||
|
||||
- name: Wait for Tailscale Network Readiness
|
||||
run: |
|
||||
echo "Waiting for Tailscale network to be ready..."
|
||||
max_wait=60
|
||||
elapsed=0
|
||||
|
||||
while [ $elapsed -lt $max_wait ]; do
|
||||
if tailscale status --json | jq -e '.BackendState == "Running"' > /dev/null 2>&1; then
|
||||
echo "Tailscale network is ready"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
done
|
||||
|
||||
if [ $elapsed -ge $max_wait ]; then
|
||||
echo "Tailscale network not ready after ${max_wait}s"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Configure SSH & Check Connection
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "Host *
|
||||
UserKnownHostsFile=/dev/null
|
||||
StrictHostKeyChecking no" > ~/.ssh/config
|
||||
chmod 644 ~/.ssh/config
|
||||
|
||||
validate_connection() {
|
||||
local machine_name=$1
|
||||
local max_retries=3
|
||||
local retry_delay=5
|
||||
|
||||
for attempt in $(seq 1 $max_retries); do
|
||||
echo "Connection attempt $attempt/$max_retries to $machine_name"
|
||||
|
||||
if ! tailscale status | grep -q "$machine_name"; then
|
||||
echo "Machine $machine_name not found in Tailscale network"
|
||||
if [ "$attempt" -eq "$max_retries" ]; then
|
||||
return 1
|
||||
fi
|
||||
sleep "$retry_delay"
|
||||
continue
|
||||
fi
|
||||
|
||||
MACHINE_IP=$(tailscale ip -4 "$machine_name")
|
||||
if ssh -o ConnectTimeout=10 -o BatchMode=yes "$TS_USERNAME@$MACHINE_IP" "echo 'Connection test'; docker --version" > /dev/null 2>&1; then
|
||||
echo "Successfully validated connection to $machine_name"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "SSH validation failed for $machine_name"
|
||||
if [ "$attempt" -lt "$max_retries" ]; then
|
||||
sleep "$retry_delay"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Failed to establish connection to $machine_name after $max_retries attempts"
|
||||
return 1
|
||||
}
|
||||
|
||||
echo -e "\nLOG:Validating connection to $TS_MACHINE_NAME..."
|
||||
if ! validate_connection "$TS_MACHINE_NAME"; then
|
||||
echo "Error: Failed to establish reliable connection to $TS_MACHINE_NAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Deploy with Docker Stack
|
||||
env:
|
||||
AGE_ENCRYPTED_ASC_SECRETS: ${{ secrets.AGE_ENCRYPTED_ASC_SECRETS }}
|
||||
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
||||
# Variable set from GitHub "Environment" secrets (AGE encrypted)
|
||||
# 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
|
||||
# SES_SMTP_USERNAME
|
||||
# SES_SMTP_PASSWORD
|
||||
# GROWTHBOOK_FASTIFY_API_HOST
|
||||
# GROWTHBOOK_FASTIFY_CLIENT_KEY
|
||||
# HOME_LOCATION
|
||||
# API_LOCATION
|
||||
# SOCRATES_API_KEY
|
||||
# SOCRATES_ENDPOINT
|
||||
# STRIPE_SECRET_KEY
|
||||
# Variables set from SetupJob
|
||||
DEPLOYMENT_VERSION: ${{ needs.build.outputs.tagname }}
|
||||
DEPLOYMENT_ENV: ${{ needs.setup-jobs.outputs.tgt_env_long }}
|
||||
DEPLOYMENT_TLD: ${{ needs.setup-jobs.outputs.site_tld }}
|
||||
FCC_API_LOG_LEVEL: ${{ needs.setup-jobs.outputs.api_log_lvl }}
|
||||
SHOW_UPCOMING_CHANGES: ${{ needs.setup-jobs.outputs.show_upcoming_changes }}
|
||||
# Stack name
|
||||
STACK_NAME: ${{ needs.setup-jobs.outputs.tgt_env_short }}-api
|
||||
run: |
|
||||
REMOTE_SCRIPT="
|
||||
set -e
|
||||
trap 'rm -f .env age.key secrets.age.asc .env.tmp' EXIT
|
||||
echo -e '\nLOG:Deploying API to $TS_MACHINE_NAME...'
|
||||
cd /home/$TS_USERNAME/docker-swarm-config/stacks/api
|
||||
|
||||
echo -e '\nLOG:Checking if age is installed...'
|
||||
which age > /dev/null
|
||||
|
||||
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: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:Adding deployment variables...'
|
||||
{
|
||||
echo \"DEPLOYMENT_VERSION=$DEPLOYMENT_VERSION\"
|
||||
echo \"DEPLOYMENT_TLD=$DEPLOYMENT_TLD\"
|
||||
echo \"DEPLOYMENT_ENV=$DEPLOYMENT_ENV\"
|
||||
echo \"FCC_API_LOG_LEVEL=$FCC_API_LOG_LEVEL\"
|
||||
echo \"SHOW_UPCOMING_CHANGES=$SHOW_UPCOMING_CHANGES\"
|
||||
} >> .env
|
||||
|
||||
echo -e '\nLOG:Sourcing environment...'
|
||||
REQUIRED_VARS=(
|
||||
\"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\"
|
||||
\"SES_SMTP_USERNAME\"
|
||||
\"SES_SMTP_PASSWORD\"
|
||||
\"GROWTHBOOK_FASTIFY_API_HOST\"
|
||||
\"GROWTHBOOK_FASTIFY_CLIENT_KEY\"
|
||||
\"HOME_LOCATION\"
|
||||
\"API_LOCATION\"
|
||||
\"SOCRATES_API_KEY\"
|
||||
\"SOCRATES_ENDPOINT\"
|
||||
\"STRIPE_SECRET_KEY\"
|
||||
\"DEPLOYMENT_VERSION\"
|
||||
\"DEPLOYMENT_TLD\"
|
||||
\"DEPLOYMENT_ENV\"
|
||||
\"FCC_API_LOG_LEVEL\"
|
||||
)
|
||||
|
||||
while IFS='=' read -r key value; do
|
||||
if [[ -n \"\$key\" && ! \"\$key\" =~ ^# ]]; then
|
||||
export \"\${key}=\${value}\"
|
||||
fi
|
||||
done < .env
|
||||
|
||||
MISSING_VARS=()
|
||||
for var in \"\${REQUIRED_VARS[@]}\"; do
|
||||
if [[ -z \"\${!var}\" ]]; then
|
||||
MISSING_VARS+=(\"\$var\")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ \${#MISSING_VARS[@]} -gt 0 ]]; then
|
||||
echo \"ERROR: The following required environment variables are missing or empty:\"
|
||||
for var in \"\${MISSING_VARS[@]}\"; do
|
||||
echo \" - \$var\"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf .env
|
||||
|
||||
echo -e '\nLOG:Validating deployment version...'
|
||||
if [[ \"\$DEPLOYMENT_VERSION\" != \"$DEPLOYMENT_VERSION\" ]]; then
|
||||
echo \"Error: Version mismatch. Expected: $DEPLOYMENT_VERSION, Got: \$DEPLOYMENT_VERSION\"
|
||||
exit 1
|
||||
fi
|
||||
env | grep -E 'DEPLOYMENT_VERSION'
|
||||
|
||||
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 for debugging...'
|
||||
CONFIG_OUTPUT=\"\$CONFIG_FILENAME\"
|
||||
fi
|
||||
docker stack config -c stack-api.yml > \$CONFIG_OUTPUT
|
||||
|
||||
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")
|
||||
# shellcheck disable=SC2029 # client-side expansion of REMOTE_SCRIPT is intended
|
||||
ssh "$TS_USERNAME@$MACHINE_IP" "$REMOTE_SCRIPT"
|
||||
|
||||
# TODO(o11y): Sentry release finalize disabled pending a follow-up PR with
|
||||
# a stable release-id scheme (source-map upload in docker-docr.yml is
|
||||
# disabled alongside). Re-enabling should also gate on a rollback-safe
|
||||
# convergence check (docker service state) before finalizing, since
|
||||
# `docker stack deploy --detach=false` exits 0 through a healthcheck
|
||||
# rollback. See .scratchpad/o11y-cutover-review-2026-07-10.md.
|
||||
# - name: Finalize Sentry release
|
||||
# env:
|
||||
# SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
# SENTRY_ORG: freecodecamp
|
||||
# SENTRY_PROJECT: api-fastify
|
||||
# RELEASE: ${{ needs.build.outputs.tagname }}
|
||||
# DEPLOY_ENV: ${{ needs.setup-jobs.outputs.tgt_env_long }}
|
||||
# run: |
|
||||
# npx --yes @sentry/cli@3.6.0 releases finalize "$RELEASE"
|
||||
# npx --yes @sentry/cli@3.6.0 releases deploys "$RELEASE" new -e "$DEPLOY_ENV"
|
||||
Reference in New Issue
Block a user