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,381 @@
|
||||
name: CD - Deploy - Clients
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target_language:
|
||||
description: 'Target language (or "all" for all languages)'
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- english
|
||||
- chinese
|
||||
- espanol
|
||||
- chinese-traditional
|
||||
- italian
|
||||
- portuguese
|
||||
- ukrainian
|
||||
- japanese
|
||||
- german
|
||||
- swahili
|
||||
- korean
|
||||
default: all
|
||||
show_upcoming_changes:
|
||||
description: 'Show upcoming changes (enables upcoming certifications and challenges)'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: deploy-client-${{ 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 }} # org, dev
|
||||
tgt_env_short: ${{ steps.setup.outputs.tgt_env_short }} # prd, stg
|
||||
tgt_env_long: ${{ steps.setup.outputs.tgt_env_long }} # production, staging
|
||||
tgt_env_branch: ${{ steps.setup.outputs.tgt_env_branch }} # prod-current, prod-staging
|
||||
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 }}
|
||||
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 "tgt_env_branch=prod-current" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
*)
|
||||
echo "site_tld=dev" >> $GITHUB_OUTPUT
|
||||
echo "tgt_env_short=stg" >> $GITHUB_OUTPUT
|
||||
echo "tgt_env_long=staging" >> $GITHUB_OUTPUT
|
||||
echo "tgt_env_branch=prod-staging" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
esac
|
||||
|
||||
setup-matrix:
|
||||
name: Setup Matrix
|
||||
runs-on: ubuntu-24.04
|
||||
needs: setup-jobs
|
||||
outputs:
|
||||
matrix: ${{ steps.matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Setup Matrix
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
id: matrix
|
||||
env:
|
||||
TARGET_LANG: ${{ inputs.target_language }}
|
||||
with:
|
||||
script: |
|
||||
// Constants
|
||||
const NODE_VERSION = 24;
|
||||
|
||||
// Input sanitization and validation
|
||||
const rawTargetLang = process.env.TARGET_LANG || 'all';
|
||||
const targetLang = rawTargetLang.trim().toLowerCase();
|
||||
console.log(`Target language: ${targetLang}`);
|
||||
|
||||
// Language mappings (single source of truth)
|
||||
const languageMap = {
|
||||
'english': 'eng',
|
||||
'chinese': 'chn',
|
||||
'espanol': 'esp',
|
||||
'chinese-traditional': 'cnt',
|
||||
'italian': 'ita',
|
||||
'portuguese': 'por',
|
||||
'ukrainian': 'ukr',
|
||||
'japanese': 'jpn',
|
||||
'german': 'ger',
|
||||
'swahili': 'swa',
|
||||
'korean': 'kor'
|
||||
};
|
||||
|
||||
const allLanguages = Object.keys(languageMap);
|
||||
let matrix;
|
||||
|
||||
if (targetLang === 'all') {
|
||||
console.log('Building matrix for all languages');
|
||||
console.log(`Available languages: ${allLanguages.join(', ')}`);
|
||||
|
||||
// Build include array for all languages
|
||||
const include = allLanguages.map(lang => ({
|
||||
'node-version': NODE_VERSION,
|
||||
'lang-name-full': lang,
|
||||
'lang-name-short': languageMap[lang]
|
||||
}));
|
||||
|
||||
matrix = {
|
||||
include: include
|
||||
};
|
||||
|
||||
} else {
|
||||
console.log(`Building matrix for single language: ${targetLang}`);
|
||||
|
||||
// Validate language selection
|
||||
if (!languageMap[targetLang]) {
|
||||
const errorMsg = `Unknown language '${targetLang}'. Available: ${allLanguages.join(', ')}`;
|
||||
console.error(errorMsg);
|
||||
core.setFailed(errorMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Processing: ${targetLang} -> ${languageMap[targetLang]}`);
|
||||
|
||||
// Create single language matrix
|
||||
matrix = {
|
||||
include: [{
|
||||
'node-version': NODE_VERSION,
|
||||
'lang-name-full': targetLang,
|
||||
'lang-name-short': languageMap[targetLang]
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
// Final validation
|
||||
if (!matrix || !matrix.include || matrix.include.length === 0) {
|
||||
core.setFailed('Generated matrix is empty or invalid');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Generated matrix:');
|
||||
console.log(JSON.stringify(matrix, null, 2));
|
||||
console.log(`Matrix will create ${matrix.include.length} job(s)`);
|
||||
|
||||
// Set output for GitHub Actions
|
||||
core.setOutput('matrix', JSON.stringify(matrix));
|
||||
|
||||
client:
|
||||
name: Clients - [${{ needs.setup-jobs.outputs.tgt_env_short }}] [${{ matrix.lang-name-short }}]
|
||||
needs: [setup-jobs, setup-matrix]
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.setup-matrix.outputs.matrix) }}
|
||||
permissions:
|
||||
deployments: write
|
||||
contents: read
|
||||
environment:
|
||||
name: ${{ needs.setup-jobs.outputs.tgt_env_short }}-clients
|
||||
env:
|
||||
TS_USERNAME: ${{ secrets.TS_USERNAME }}
|
||||
TS_MACHINE_NAME_PREFIX: ${{ secrets.TS_MACHINE_NAME_PREFIX }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@91ab88e2619ed1f46221f0ba42d1492c02baf788 # v6
|
||||
id: pnpm-install
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Language specific ENV - [${{ matrix.lang-name-full }}]
|
||||
run: |
|
||||
if [ "${{ matrix.lang-name-full }}" = "english" ]; then
|
||||
echo "HOME_LOCATION=https://www.freecodecamp.${{ needs.setup-jobs.outputs.site_tld }}" >> $GITHUB_ENV
|
||||
echo "NEWS_LOCATION=https://www.freecodecamp.${{ needs.setup-jobs.outputs.site_tld }}/news" >> $GITHUB_ENV
|
||||
else
|
||||
echo "HOME_LOCATION=https://www.freecodecamp.${{ needs.setup-jobs.outputs.site_tld }}/${{ matrix.lang-name-full }}" >> $GITHUB_ENV
|
||||
echo "NEWS_LOCATION=https://www.freecodecamp.${{ needs.setup-jobs.outputs.site_tld }}/${{ matrix.lang-name-full }}/news" >> $GITHUB_ENV
|
||||
fi
|
||||
echo "CLIENT_LOCALE=${{ matrix.lang-name-full }}" >> $GITHUB_ENV
|
||||
echo "CURRICULUM_LOCALE=${{ matrix.lang-name-full }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Create deployment version
|
||||
id: deployment-version
|
||||
run: |
|
||||
DEPLOYMENT_VERSION=$(git rev-parse --short HEAD)-$(date +%Y%m%d)-$(date +%H%M)
|
||||
echo "DEPLOYMENT_VERSION=$DEPLOYMENT_VERSION" >> $GITHUB_ENV
|
||||
echo "DEPLOYMENT_VERSION=$DEPLOYMENT_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install and Build
|
||||
env:
|
||||
API_LOCATION: 'https://api.freecodecamp.${{ needs.setup-jobs.outputs.site_tld }}'
|
||||
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
GROWTHBOOK_URI: ${{ secrets.GROWTHBOOK_URI }}
|
||||
FORUM_LOCATION: 'https://forum.freecodecamp.org'
|
||||
PATREON_CLIENT_ID: ${{ secrets.PATREON_CLIENT_ID }}
|
||||
PAYPAL_CLIENT_ID: ${{ secrets.PAYPAL_CLIENT_ID }}
|
||||
STRIPE_PUBLIC_KEY: ${{ secrets.STRIPE_PUBLIC_KEY }}
|
||||
SHOW_UPCOMING_CHANGES: ${{ needs.setup-jobs.outputs.show_upcoming_changes }}
|
||||
FREECODECAMP_NODE_ENV: production
|
||||
# The below is used in ecosystem.config.js file for the API -- to be removed later
|
||||
DEPLOYMENT_ENV: ${{ needs.setup-jobs.outputs.tgt_env_long }}
|
||||
# The above is used in ecosystem.config.js file for the API -- to be removed later
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm run build
|
||||
|
||||
- name: Tar Files
|
||||
run: tar -czf client-${{ matrix.lang-name-short }}.tar client/public
|
||||
|
||||
- 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}}-clients-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'; uptime" > /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 connections to all machines..."
|
||||
for i in {0..1}; do
|
||||
TS_MACHINE_NAME=${TS_MACHINE_NAME_PREFIX}-${{ matrix.lang-name-short }}-${i}
|
||||
echo "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
|
||||
done
|
||||
echo "All machine connections validated successfully"
|
||||
|
||||
- name: Upload and Deploy
|
||||
run: |
|
||||
for i in {0..1}; do
|
||||
TS_MACHINE_NAME=${TS_MACHINE_NAME_PREFIX}-${{ matrix.lang-name-short }}-${i}
|
||||
CURRENT_DATE=$(date +%Y%m%d)
|
||||
CLIENT_SRC=client-${{ matrix.lang-name-short }}.tar
|
||||
CLIENT_DST=/tmp/client-${{ matrix.lang-name-short }}-${CURRENT_DATE}-${{ github.run_id }}.tar
|
||||
CLIENT_BINARIES=${{needs.setup-jobs.outputs.tgt_env_short}}-release-$CURRENT_DATE-${{ github.run_id }}
|
||||
|
||||
echo -e "\nLOG:Uploading client archive to $TS_MACHINE_NAME..."
|
||||
MACHINE_IP=$(tailscale ip -4 $TS_MACHINE_NAME)
|
||||
scp $CLIENT_SRC $TS_USERNAME@$MACHINE_IP:$CLIENT_DST
|
||||
|
||||
REMOTE_SCRIPT="
|
||||
set -e
|
||||
echo -e '\nLOG: Deploying client - $CLIENT_BINARIES to $TS_MACHINE_NAME...'
|
||||
|
||||
echo -e '\nLOG:Extracting client archive...'
|
||||
mkdir -p /home/$TS_USERNAME/client/releases/$CLIENT_BINARIES
|
||||
tar -xzf $CLIENT_DST -C /home/$TS_USERNAME/client/releases/$CLIENT_BINARIES --strip-components=2
|
||||
|
||||
echo -e '\nLOG:Cleaning up client archive...'
|
||||
rm $CLIENT_DST
|
||||
|
||||
echo -e '\nLOG:Checking client archive size...'
|
||||
du -sh /home/$TS_USERNAME/client/releases/$CLIENT_BINARIES
|
||||
|
||||
echo -e '\nLOG:Environment setup...'
|
||||
cd /home/$TS_USERNAME/client
|
||||
export NVM_DIR=\$HOME/.nvm && [ -s "\$NVM_DIR/nvm.sh" ] && source "\$NVM_DIR/nvm.sh"
|
||||
echo -e '\nLOG:Checking available Node.js versions...'
|
||||
nvm ls | grep 'default'
|
||||
echo -e '\nLOG:Checking Node.js version...'
|
||||
node --version
|
||||
|
||||
echo -e '\nLOG:Installing serve...'
|
||||
npm install -g serve@13
|
||||
|
||||
echo -e '\nLOG:Primary client setup...'
|
||||
rm -f client-start-primary.sh
|
||||
echo \"serve -c ../../serve.json releases/$CLIENT_BINARIES -p 50505\" >> client-start-primary.sh
|
||||
chmod +x client-start-primary.sh
|
||||
pm2 delete client-primary || true
|
||||
pm2 start ./client-start-primary.sh --name client-primary
|
||||
echo -e '\nLOG:Primary client setup completed.'
|
||||
|
||||
pm2 ls
|
||||
|
||||
echo -e '\nLOG:Secondary client setup...'
|
||||
rm -f client-start-secondary.sh
|
||||
echo \"serve -c ../../serve.json releases/$CLIENT_BINARIES -p 52525\" >> client-start-secondary.sh
|
||||
chmod +x client-start-secondary.sh
|
||||
pm2 delete client-secondary || true
|
||||
pm2 start ./client-start-secondary.sh --name client-secondary
|
||||
echo -e '\nLOG:Secondary client setup completed.'
|
||||
|
||||
pm2 ls
|
||||
pm2 save
|
||||
|
||||
echo -e '\nLOG:Finished deployment.'
|
||||
"
|
||||
ssh $TS_USERNAME@$MACHINE_IP "$REMOTE_SCRIPT"
|
||||
done
|
||||
Reference in New Issue
Block a user