Merge branch 'v28' into mls-4-11

This commit is contained in:
iamlewis
2025-12-26 10:29:52 +00:00
committed by GitHub
19 changed files with 253 additions and 678 deletions
+2 -1
View File
@@ -9,7 +9,8 @@ LICENSE
.vscode
Makefile
helm-charts
.env
.env*
.editorconfig
.idea
coverage*
tests/
+6 -12
View File
@@ -84,11 +84,12 @@ jobs:
token: ${{ steps.generate-token.outputs.token }}
environment-url: https://${{ env.FQDN }}
environment: ${{ inputs.target_domain == 'openfront.io' && 'prod' || 'staging' }}
- name: 🔗 Log in to Docker Hub
- name: 🔗 Log in to GHCR
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
registry: ghcr.io
username: ${{ vars.GHCR_USERNAME }}
password: ${{ secrets.GHCR_TOKEN }}
- name: 🔑 Create SSH private key
env:
SERVER_HOST_MASTERS: ${{ secrets.SERVER_HOST_MASTERS }}
@@ -105,21 +106,14 @@ jobs:
chmod 600 ~/.ssh/id_rsa
- name: 🚢 Deploy
env:
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
DOCKER_REPO: ${{ vars.DOCKERHUB_REPO }}
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
GHCR_REPO: ${{ vars.GHCR_REPO }}
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
ENV: ${{ inputs.target_domain == 'openfront.io' && 'prod' || 'staging' }}
HOST: ${{ github.event_name == 'workflow_dispatch' && inputs.target_host || 'staging' }}
OTEL_ENDPOINT: ${{ secrets.OTEL_ENDPOINT }}
OTEL_PASSWORD: ${{ secrets.OTEL_PASSWORD }}
OTEL_USERNAME: ${{ secrets.OTEL_USERNAME }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
R2_ACCESS_KEY: ${{ secrets.R2_ACCESS_KEY }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }}
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
API_KEY: ${{ secrets.API_KEY }}
SERVER_HOST_MASTERS: ${{ secrets.SERVER_HOST_MASTERS }}
+13 -40
View File
@@ -19,12 +19,13 @@ jobs:
- name: 🔗 Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
registry: ghcr.io
username: ${{ vars.GHCR_USERNAME }}
password: ${{ secrets.GHCR_TOKEN }}
- id: build
env:
DOCKER_REPO: openfront-prod
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
GHCR_REPO: openfront-prod
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
RELEASE_BODY: ${{ github.event.release.body }}
RELEASE_NAME: ${{ github.event.release.name }}
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
@@ -63,21 +64,14 @@ jobs:
chmod 600 ~/.ssh/id_rsa
- name: 🚀 Deploy image
env:
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
DOCKER_REPO: openfront-prod
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
GHCR_REPO: openfront-prod
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
DOMAIN: ${{ vars.DOMAIN }}
IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }}
OTEL_ENDPOINT: ${{ secrets.OTEL_ENDPOINT }}
OTEL_PASSWORD: ${{ secrets.OTEL_PASSWORD }}
OTEL_USERNAME: ${{ secrets.OTEL_USERNAME }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
R2_ACCESS_KEY: ${{ secrets.R2_ACCESS_KEY }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }}
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
API_KEY: ${{ secrets.API_KEY }}
SERVER_HOST_STAGING: ${{ secrets.SERVER_HOST_STAGING }}
@@ -121,21 +115,14 @@ jobs:
chmod 600 ~/.ssh/id_rsa
- name: 🚀 Deploy image
env:
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
DOCKER_REPO: ${{ vars.DOCKERHUB_REPO }}
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
GHCR_REPO: ${{ vars.GHCR_REPO }}
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
DOMAIN: ${{ vars.DOMAIN }}
IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }}
OTEL_ENDPOINT: ${{ secrets.OTEL_ENDPOINT }}
OTEL_PASSWORD: ${{ secrets.OTEL_PASSWORD }}
OTEL_USERNAME: ${{ secrets.OTEL_USERNAME }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
R2_ACCESS_KEY: ${{ secrets.R2_ACCESS_KEY }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }}
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
API_KEY: ${{ secrets.API_KEY }}
SERVER_HOST_FALK1: ${{ secrets.SERVER_HOST_FALK1 }}
@@ -179,21 +166,14 @@ jobs:
chmod 600 ~/.ssh/id_rsa
- name: 🚀 Deploy image
env:
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
DOCKER_REPO: ${{ vars.DOCKERHUB_REPO }}
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
GHCR_REPO: ${{ vars.GHCR_REPO }}
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
DOMAIN: ${{ vars.DOMAIN }}
IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }}
OTEL_ENDPOINT: ${{ secrets.OTEL_ENDPOINT }}
OTEL_PASSWORD: ${{ secrets.OTEL_PASSWORD }}
OTEL_USERNAME: ${{ secrets.OTEL_USERNAME }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
R2_ACCESS_KEY: ${{ secrets.R2_ACCESS_KEY }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }}
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
API_KEY: ${{ secrets.API_KEY }}
SERVER_HOST_FALK1: ${{ secrets.SERVER_HOST_FALK1 }}
@@ -237,21 +217,14 @@ jobs:
chmod 600 ~/.ssh/id_rsa
- name: 🚀 Deploy image
env:
ADMIN_TOKEN: ${{ secrets.ADMIN_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
DOCKER_REPO: ${{ vars.DOCKERHUB_REPO }}
DOCKER_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
GHCR_REPO: ${{ vars.GHCR_REPO }}
GHCR_USERNAME: ${{ vars.GHCR_USERNAME }}
DOMAIN: ${{ vars.DOMAIN }}
IMAGE_ID: ${{ needs.build.outputs.IMAGE_ID }}
OTEL_ENDPOINT: ${{ secrets.OTEL_ENDPOINT }}
OTEL_PASSWORD: ${{ secrets.OTEL_PASSWORD }}
OTEL_USERNAME: ${{ secrets.OTEL_USERNAME }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_AUTH_HEADER: ${{ secrets.OTEL_AUTH_HEADER }}
R2_ACCESS_KEY: ${{ secrets.R2_ACCESS_KEY }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }}
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
API_KEY: ${{ secrets.API_KEY }}
SERVER_HOST_FALK1: ${{ secrets.SERVER_HOST_FALK1 }}
+65 -67
View File
@@ -1,98 +1,96 @@
# Use an official Node runtime as the base image
FROM node:24-slim AS base
# Set the working directory in the container
WORKDIR /usr/src/app
# Create dependency layer
FROM base AS dependencies
# Build stage - install ALL dependencies and build
FROM base AS build
ENV HUSKY=0
# Copy package files first for better caching
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Copy only what's needed for build
COPY tsconfig.json ./
COPY tsconfig.jest.json ./
COPY webpack.config.js ./
COPY tailwind.config.js ./
COPY postcss.config.js ./
COPY eslint.config.js ./
COPY resources ./resources
COPY proprietary ./proprietary
COPY src ./src
ARG GIT_COMMIT=unknown
ENV GIT_COMMIT="$GIT_COMMIT"
RUN npm run build-prod
# Production dependencies stage - separate from build
FROM base AS prod-deps
ENV HUSKY=0
ENV NPM_CONFIG_IGNORE_SCRIPTS=1
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev
# Final production image
FROM base
# Install system dependencies
RUN apt-get update && apt-get install -y \
nginx \
git \
curl \
jq \
wget \
supervisor \
apache2-utils \
&& rm -rf /var/lib/apt/lists/*
# Update worker_connections in the existing nginx.conf
RUN sed -i 's/worker_connections [0-9]*/worker_connections 8192/' /etc/nginx/nginx.conf
FROM dependencies AS build
ARG GIT_COMMIT=unknown
ENV GIT_COMMIT="$GIT_COMMIT"
# Disable Husky hooks
ENV HUSKY=0
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy the rest of the application code
COPY . .
# Build the client-side application
RUN npm run build-prod
# So we can see which commit was used to build the container
# https://openfront.io/commit.txt
RUN echo "$GIT_COMMIT" > static/commit.txt
# Remove maps data from final image
FROM base AS prod-files
COPY . .
RUN rm -rf resources/maps
FROM dependencies AS npm-dependencies
# Disable Husky hooks
ENV HUSKY=0
ENV NPM_CONFIG_IGNORE_SCRIPTS=1
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm ci --omit=dev
# Final image
FROM base
ARG GIT_COMMIT=unknown
ENV GIT_COMMIT="$GIT_COMMIT"
RUN apt-get update && apt-get install -y \
nginx \
supervisor \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy installed packages from dependencies stage
RUN curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb > cloudflared.deb \
&& dpkg -i cloudflared.deb \
&& rm cloudflared.deb
# Copy Nginx configuration and ensure it's used instead of the default
COPY nginx.conf /etc/nginx/conf.d/default.conf
RUN rm -f /etc/nginx/sites-enabled/default
COPY --from=dependencies /etc/nginx/nginx.conf /etc/nginx/nginx.conf
# Update worker_connections in nginx.conf
RUN sed -i 's/worker_connections [0-9]*/worker_connections 8192/' /etc/nginx/nginx.conf
# Copy npm dependencies
COPY --from=npm-dependencies /usr/src/app/node_modules node_modules
COPY package.json .
# Copy the rest of the application code
COPY --from=prod-files /usr/src/app/ /usr/src/app/
# Copy frontend
COPY --from=build /usr/src/app/static static
# Create cloudflared directory with proper permissions
RUN mkdir -p /etc/cloudflared && \
chown -R node:node /etc/cloudflared && \
chmod -R 755 /etc/cloudflared
# Setup supervisor configuration
RUN mkdir -p /var/log/supervisor
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Copy Nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
RUN rm -f /etc/nginx/sites-enabled/default
# Copy and make executable the startup script
COPY startup.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/startup.sh
RUN mkdir -p /etc/cloudflared && \
chown -R node:node /etc/cloudflared && \
chmod -R 755 /etc/cloudflared
# Copy production node_modules from prod-deps stage (cached separately from build)
COPY --from=prod-deps /usr/src/app/node_modules ./node_modules
COPY package*.json ./
# Set Cloudflared config directory to a volume mount location
# Copy built artifacts from build stage
COPY --from=build /usr/src/app/static ./static
COPY resources ./resources
# Remove maps because they are not used by the server.
RUN rm -rf ./resources/maps
COPY tsconfig.json ./
COPY src ./src
ARG GIT_COMMIT=unknown
RUN echo "$GIT_COMMIT" > static/commit.txt
ENV GIT_COMMIT="$GIT_COMMIT"
ENV CF_CONFIG_PATH=/etc/cloudflared/config.yml
ENV CF_CREDS_PATH=/etc/cloudflared/creds.json
# Use the startup script as the entrypoint
ENTRYPOINT ["/usr/local/bin/startup.sh"]
ENTRYPOINT ["/usr/local/bin/startup.sh"]
+5 -22
View File
@@ -15,34 +15,34 @@ print_header "BUILD AND DEPLOY WRAPPER"
echo "This script will run build.sh and deploy.sh in sequence."
echo "You can also run them separately:"
echo " ./build.sh [prod|staging] [version_tag]"
echo " ./deploy.sh [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain] [--enable_basic_auth]"
echo " ./deploy.sh [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain]"
echo ""
# Check command line arguments
if [ $# -lt 3 ] || [ $# -gt 5 ]; then
echo "Error: Please specify environment, host, and subdomain"
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain] [--enable_basic_auth]"
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain]"
exit 1
fi
# Validate first argument (environment)
if [ "$1" != "prod" ] && [ "$1" != "staging" ]; then
echo "Error: First argument must be either 'prod' or 'staging'"
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain] [--enable_basic_auth]"
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain]"
exit 1
fi
# Validate second argument (host)
if [ "$2" != "falk1" ] && [ "$2" != "nbg1" ] && [ "$2" != "staging" ] && [ "$2" != "masters" ]; then
echo "Error: Second argument must be either 'falk1', 'nbg1', 'staging', or 'masters'"
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain] [--enable_basic_auth]"
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain]"
exit 1
fi
# Validate third argument (subdomain)
if [ -z "$3" ]; then
echo "Error: Subdomain is required"
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain] [--enable_basic_auth]"
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain]"
exit 1
fi
@@ -54,23 +54,6 @@ echo "Generated version tag: $VERSION_TAG"
ENV="$1"
HOST="$2"
SUBDOMAIN="$3"
ENABLE_BASIC_AUTH=""
# Parse remaining arguments
shift 3
while [[ $# -gt 0 ]]; do
case $1 in
--enable_basic_auth)
ENABLE_BASIC_AUTH="--enable_basic_auth"
shift
;;
*)
echo "Error: Unknown argument: $1"
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [subdomain] [--enable_basic_auth]"
exit 1
;;
esac
done
# Step 1: Run build.sh
echo "Step 1: Running build.sh..."
+33 -14
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# build.sh - Build and upload Docker image to Docker Hub
# build.sh - Build and upload image to GitHub Container Registry
# This script:
# 1. Builds and uploads the Docker image to Docker Hub with appropriate tag
# 1. Builds and uploads the image to GitHub Container Registry with appropriate tag
# 2. Optionally saves container metadata to a file (if METADATA_FILE is provided as 3rd argument)
set -e # Exit immediately if a command exits with a non-zero status
@@ -57,23 +57,22 @@ if [ -f .env.$DEPLOY_ENV ]; then
fi
# Check required environment variables for build
if [ -z "$DOCKER_USERNAME" ] || [ -z "$DOCKER_REPO" ]; then
echo "Error: DOCKER_USERNAME or DOCKER_REPO not defined in .env file or environment"
if [ -z "$GHCR_USERNAME" ] || [ -z "$GHCR_REPO" ]; then
echo "Error: GHCR_USERNAME or GHCR_REPO not defined in .env file or environment"
exit 1
fi
DOCKER_IMAGE="${DOCKER_USERNAME}/${DOCKER_REPO}:${VERSION_TAG}"
GHCR_IMAGE="${GHCR_USERNAME}/${GHCR_REPO}:${VERSION_TAG}"
# If ADDITIONAL_VERSION_TAG is provided ADDITIONAL_DOCKER_IMAGE will be set
# If ADDITIONAL_VERSION_TAG is provided ADDITIONAL_GHCR_IMAGE will be set
# example usage: adding latest tag
if [ -n "$ADDITIONAL_VERSION_TAG" ]; then
ADDITIONAL_DOCKER_IMAGE="${DOCKER_USERNAME}/${DOCKER_REPO}:${ADDITIONAL_VERSION_TAG}"
ADDITIONAL_GHCR_IMAGE="${GHCR_USERNAME}/${GHCR_REPO}:${ADDITIONAL_VERSION_TAG}"
fi
# Build and upload Docker image to Docker Hub
echo "Environment: ${DEPLOY_ENV}"
echo "Using version tag: $VERSION_TAG"
echo "Docker repository: $DOCKER_REPO"
echo "Docker repository: $GHCR_REPO"
echo "Metadata file: $METADATA_FILE"
# Get Git commit for build info
@@ -87,12 +86,32 @@ if [ -n "$VERSION_TXT" ]; then
echo "$VERSION_TXT" > resources/version.txt
fi
# Set up cache image reference
CACHE_IMAGE="${GHCR_USERNAME}/${GHCR_REPO}:latest"
BUILDCACHE_IMAGE="${GHCR_USERNAME}/${GHCR_REPO}:buildcache"
echo "Building with buildx and registry cache..."
# Create buildx builder with docker-container driver if it doesn't exist
if ! docker buildx inspect cache-builder > /dev/null 2>&1; then
echo "Creating buildx builder..."
docker buildx create --name cache-builder --driver docker-container --use
else
echo "Using existing buildx builder..."
docker buildx use cache-builder
fi
# Use buildx with registry cache for best performance
# --push will push all tags automatically
docker buildx build \
--platform linux/amd64 \
--build-arg GIT_COMMIT=$GIT_COMMIT \
--metadata-file $METADATA_FILE \
-t $DOCKER_IMAGE \
${ADDITIONAL_DOCKER_IMAGE:+-t "$ADDITIONAL_DOCKER_IMAGE"} \
--build-arg GIT_COMMIT=$GIT_COMMIT \
--cache-from type=registry,ref=$BUILDCACHE_IMAGE \
--cache-to type=registry,ref=$BUILDCACHE_IMAGE,mode=max \
--tag $GHCR_IMAGE \
--tag $CACHE_IMAGE \
${ADDITIONAL_GHCR_IMAGE:+--tag "$ADDITIONAL_GHCR_IMAGE"} \
--push \
.
@@ -102,6 +121,6 @@ if [ $? -ne 0 ]; then
fi
echo "✅ Docker image built and pushed successfully."
echo "Image: $DOCKER_IMAGE"
echo "Image: $GHCR_IMAGE"
print_header "BUILD COMPLETED SUCCESSFULLY ${DOCKER_IMAGE}"
print_header "BUILD COMPLETED SUCCESSFULLY ${GHCR_IMAGE}"
+10 -58
View File
@@ -6,27 +6,6 @@
set -e # Exit immediately if a command exits with a non-zero status
# Initialize variables
ENABLE_BASIC_AUTH=false
# Parse command line arguments
POSITIONAL_ARGS=()
while [[ $# -gt 0 ]]; do
case $1 in
--enable_basic_auth)
ENABLE_BASIC_AUTH=true
shift
;;
*)
POSITIONAL_ARGS+=("$1")
shift
;;
esac
done
# Restore positional parameters
set -- "${POSITIONAL_ARGS[@]}"
# Function to print section headers
print_header() {
echo "======================================================"
@@ -37,21 +16,21 @@ print_header() {
# Check command line arguments
if [ $# -ne 4 ]; then
echo "Error: Please specify environment, host, version tag, and subdomain"
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain] [--enable_basic_auth]"
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain]"
exit 1
fi
# Validate first argument (environment)
if [ "$1" != "prod" ] && [ "$1" != "staging" ]; then
echo "Error: First argument must be either 'prod' or 'staging'"
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain] [--enable_basic_auth]"
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain]"
exit 1
fi
# Validate second argument (host)
if [ "$2" != "falk1" ] && [ "$2" != "nbg1" ] && [ "$2" != "staging" ] && [ "$2" != "masters" ]; then
echo "Error: Second argument must be either 'falk1', 'nbg1', 'staging', or 'masters'"
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain] [--enable_basic_auth]"
echo "Usage: $0 [prod|staging] [falk1|nbg1|staging|masters] [version_tag] [subdomain]"
exit 1
fi
@@ -76,15 +55,15 @@ if [ -f .env.$ENV ]; then
fi
# Check required environment variables for deployment
if [ -z "$DOCKER_USERNAME" ] || [ -z "$DOCKER_REPO" ]; then
echo "Error: DOCKER_USERNAME or DOCKER_REPO not defined in .env file or environment"
if [ -z "$GHCR_USERNAME" ] || [ -z "$GHCR_REPO" ]; then
echo "Error: GHCR_USERNAME or GHCR_REPO not defined in .env file or environment"
exit 1
fi
if [[ "$VERSION_TAG" == sha256:* ]]; then
DOCKER_IMAGE="${DOCKER_USERNAME}/${DOCKER_REPO}@${VERSION_TAG}"
GHCR_IMAGE="${GHCR_USERNAME}/${GHCR_REPO}@${VERSION_TAG}"
else
DOCKER_IMAGE="${DOCKER_USERNAME}/${DOCKER_REPO}:${VERSION_TAG}"
GHCR_IMAGE="${GHCR_USERNAME}/${GHCR_REPO}:${VERSION_TAG}"
fi
if [ "$HOST" == "staging" ]; then
@@ -107,21 +86,6 @@ if [ -z "$SERVER_HOST" ]; then
exit 1
fi
# Check if basic auth is enabled and credentials are available
if [ "$ENABLE_BASIC_AUTH" = true ]; then
print_header "BASIC AUTH ENABLED"
if [ -z "$BASIC_AUTH_USER" ] || [ -z "$BASIC_AUTH_PASS" ]; then
echo "Error: Basic Auth is enabled but BASIC_AUTH_USER or BASIC_AUTH_PASS not defined in .env file or environment"
exit 1
fi
echo "Basic Authentication will be enabled with user: $BASIC_AUTH_USER"
else
# If basic auth is not enabled, set the variables to empty to ensure they don't get used
BASIC_AUTH_USER=""
BASIC_AUTH_PASS=""
echo "Basic Authentication is disabled"
fi
# Configuration
UPDATE_SCRIPT="./update.sh" # Path to your update script
REMOTE_USER="openfront"
@@ -139,7 +103,7 @@ print_header "DEPLOYMENT INFORMATION"
echo "Environment: ${ENV}"
echo "Host: ${HOST}"
echo "Subdomain: ${SUBDOMAIN}"
echo "Docker Image: $DOCKER_IMAGE"
echo "Image: $GHCR_IMAGE"
echo "Target Server: $SERVER_HOST"
# Copy update script to Hetzner server
@@ -168,25 +132,16 @@ cat > $ENV_FILE << 'EOL'
GAME_ENV=$ENV
ENV=$ENV
HOST=$HOST
DOCKER_IMAGE=$DOCKER_IMAGE
DOCKER_TOKEN=$DOCKER_TOKEN
ADMIN_TOKEN=$ADMIN_TOKEN
GHCR_IMAGE=$GHCR_IMAGE
GHCR_TOKEN=$GHCR_TOKEN
CF_ACCOUNT_ID=$CF_ACCOUNT_ID
R2_ACCESS_KEY=$R2_ACCESS_KEY
R2_SECRET_KEY=$R2_SECRET_KEY
R2_BUCKET=$R2_BUCKET
CF_API_TOKEN=$CF_API_TOKEN
TURNSTILE_SECRET_KEY=$TURNSTILE_SECRET_KEY
API_KEY=$API_KEY
DOMAIN=$DOMAIN
SUBDOMAIN=$SUBDOMAIN
OTEL_USERNAME=$OTEL_USERNAME
OTEL_PASSWORD=$OTEL_PASSWORD
OTEL_ENDPOINT=$OTEL_ENDPOINT
OTEL_EXPORTER_OTLP_ENDPOINT=$OTEL_EXPORTER_OTLP_ENDPOINT
OTEL_AUTH_HEADER=$OTEL_AUTH_HEADER
BASIC_AUTH_USER=$BASIC_AUTH_USER
BASIC_AUTH_PASS=$BASIC_AUTH_PASS
EOL
chmod 600 $ENV_FILE && \
$REMOTE_UPDATE_SCRIPT $ENV_FILE"
@@ -198,8 +153,5 @@ fi
print_header "DEPLOYMENT COMPLETED SUCCESSFULLY"
echo "✅ New version deployed to ${ENV} environment in ${HOST} with subdomain ${SUBDOMAIN}!"
if [ "$ENABLE_BASIC_AUTH" = true ]; then
echo "🔒 Basic authentication enabled with user: $BASIC_AUTH_USER"
fi
echo "🌐 Check your server to verify the deployment."
echo "======================================================="
+3 -11
View File
@@ -2,23 +2,15 @@
SSH_KEY=~/.ssh/your-ssh-key
# Docker Configuration
DOCKER_USERNAME=username
DOCKER_REPO=your-repo-name
DOCKER_TOKEN=your_docker_token_here
# Admin credentials
ADMIN_TOKEN=your_admin_token_here
GHCR_USERNAME=username
GHCR_REPO=your-repo-name
GHCR_TOKEN=your_docker_token_here
# Cloudflare Configuration
CF_ACCOUNT_ID=your_cloudflare_account_id
CF_API_TOKEN=your_cloudflare_api_token
DOMAIN=your-domain.com
# R2 Configuration
R2_ACCESS_KEY=your_r2_access_key
R2_SECRET_KEY=your_r2_secret_key
R2_BUCKET=your-bucket-name
# API Key
API_KEY=your_api_key_here
-8
View File
@@ -46,10 +46,6 @@ export interface ServerConfig {
adminHeader(): string;
// Only available on the server
gitCommit(): string;
r2Bucket(): string;
r2Endpoint(): string;
r2AccessKey(): string;
r2SecretKey(): string;
apiKey(): string;
otelEndpoint(): string;
otelAuthHeader(): string;
@@ -59,10 +55,6 @@ export interface ServerConfig {
jwkPublicKey(): Promise<JWK>;
domain(): string;
subdomain(): string;
cloudflareAccountId(): string;
cloudflareApiToken(): string;
cloudflareConfigPath(): string;
cloudflareCredsPath(): string;
stripePublishableKey(): string;
allowedFlares(): string[] | undefined;
enableMatchmaking(): boolean;
+5 -26
View File
@@ -102,18 +102,6 @@ export abstract class DefaultServerConfig implements ServerConfig {
subdomain(): string {
return process.env.SUBDOMAIN ?? "";
}
cloudflareAccountId(): string {
return process.env.CF_ACCOUNT_ID ?? "";
}
cloudflareApiToken(): string {
return process.env.CF_API_TOKEN ?? "";
}
cloudflareConfigPath(): string {
return process.env.CF_CONFIG_PATH ?? "";
}
cloudflareCredsPath(): string {
return process.env.CF_CREDS_PATH ?? "";
}
private publicKey: JWK;
abstract jwtAudience(): string;
@@ -153,19 +141,6 @@ export abstract class DefaultServerConfig implements ServerConfig {
gitCommit(): string {
return process.env.GIT_COMMIT ?? "";
}
r2Endpoint(): string {
return `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`;
}
r2AccessKey(): string {
return process.env.R2_ACCESS_KEY ?? "";
}
r2SecretKey(): string {
return process.env.R2_SECRET_KEY ?? "";
}
r2Bucket(): string {
return process.env.R2_BUCKET ?? "";
}
apiKey(): string {
return process.env.API_KEY ?? "";
@@ -175,7 +150,11 @@ export abstract class DefaultServerConfig implements ServerConfig {
return "x-admin-key";
}
adminToken(): string {
return process.env.ADMIN_TOKEN ?? "dummy-admin-token";
const token = process.env.ADMIN_TOKEN;
if (!token) {
throw new Error("ADMIN_TOKEN not set");
}
return token;
}
abstract numWorkers(): number;
abstract env(): GameEnv;
+4 -4
View File
@@ -44,13 +44,14 @@ export class BotBehavior {
this.game.addExecution(new EmojiExecution(this.player, player.id(), emoji));
}
// Prevent attacking of humans on lower difficulties
// Prevent attacking of humans on lower difficulties (or if we are a bot)
private shouldAttack(other: Player | TerraNullius): boolean {
// Always attack Terra Nullius, non-humans and traitors
if (
other.isPlayer() === false ||
other.type() !== PlayerType.Human ||
other.isTraitor()
other.isTraitor() ||
this.player.type() === PlayerType.Bot
) {
return true;
}
@@ -374,7 +375,6 @@ export class BotBehavior {
}
// Choose a new enemy randomly
const { difficulty } = this.game.config().gameConfig();
const neighbors = this.player.neighbors();
for (const neighbor of this.random.shuffleArray(neighbors)) {
if (!neighbor.isPlayer()) continue;
@@ -383,7 +383,7 @@ export class BotBehavior {
neighbor.type() === PlayerType.FakeHuman ||
neighbor.type() === PlayerType.Human
) {
if (this.random.chance(2) || difficulty === Difficulty.Easy) {
if (this.random.chance(2)) {
continue;
}
}
-289
View File
@@ -1,289 +0,0 @@
import { spawn } from "child_process";
import { promises as fs } from "fs";
import yaml from "js-yaml";
import { logger } from "./Logger";
const log = logger.child({
module: "cloudflare",
});
export interface TunnelConfig {
domain: string;
subdomain: string;
subdomainToService: Map<string, string>;
}
interface TunnelResponse {
result: {
id: string;
token: string;
};
}
interface ZoneResponse {
result: Array<{
id: string;
}>;
}
interface DNSRecordResponse {
result: Array<{
id: string;
}>;
}
interface CloudflaredConfig {
tunnel: string;
"credentials-file": string;
ingress: Array<{
hostname?: string;
service: string;
}>;
}
export class Cloudflare {
private baseUrl = "https://api.cloudflare.com/client/v4";
constructor(
private accountId: string,
private apiToken: string,
private configPath: string,
private credsPath: string,
) {
log.info(`Using config: ${this.configPath}`);
log.info(`Using credentials: ${this.credsPath}`);
}
private async makeRequest<T>(
url: string,
method: string = "GET",
data?: any,
): Promise<T> {
const response = await fetch(url, {
method,
headers: {
Authorization: `Bearer ${this.apiToken}`,
"Content-Type": "application/json",
},
body: data ? JSON.stringify(data) : undefined,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Cloudflare API error: url ${url} ${response.status} - ${errorText}`,
);
}
return response.json() as Promise<T>;
}
public async configAlreadyExists(): Promise<boolean> {
try {
await fs.access(this.configPath);
return true;
} catch {
return false;
}
}
public async createTunnel(config: TunnelConfig): Promise<{
tunnelId: string;
tunnelToken: string;
tunnelUrl: string;
}> {
const { domain, subdomain, subdomainToService } = config;
// Generate unique tunnel name
const timestamp = new Date().toISOString().replace(/[-:.]/g, "");
const tunnelName = `${subdomain}-tunnel-${timestamp}`;
log.info(`Creating tunnel with name: ${tunnelName}`);
// Create tunnel via API to get official tunnel ID and token
const tunnelResponse = await this.makeRequest<TunnelResponse>(
`${this.baseUrl}/accounts/${this.accountId}/cfd_tunnel`,
"POST",
{ name: tunnelName },
);
const tunnelId = tunnelResponse.result.id;
const tunnelToken = tunnelResponse.result.token;
if (!tunnelId) {
throw new Error("Failed to create tunnel");
}
log.info(`Tunnel created with ID: ${tunnelId}`);
// Create local config file instead of using API configuration
await this.writeTunnelConfig(
tunnelId,
tunnelToken,
subdomain,
domain,
subdomainToService,
tunnelName,
);
// Get zone ID
const zoneResponse = await this.makeRequest<ZoneResponse>(
`${this.baseUrl}/zones?name=${domain}`,
);
const zoneId = zoneResponse.result[0]?.id;
if (!zoneId) {
throw new Error(`Could not find zone ID for domain ${domain}`);
}
await Promise.all(
Array.from(subdomainToService.entries()).map(([subdomain, _]) =>
this.updateDNSRecord(zoneId, tunnelId, subdomain, domain),
),
);
const tunnelUrl = `https://${subdomain}.${domain}`;
log.info(`Tunnel is set up! Site will be available at: ${tunnelUrl}`);
return { tunnelId, tunnelToken, tunnelUrl };
}
private async writeTunnelConfig(
tunnelId: string,
tunnelToken: string,
subdomain: string,
domain: string,
subdomainToService: Map<string, string>,
tunnelName: string,
): Promise<void> {
log.info(`Creating local config for tunnel ${subdomain}.${domain}...`);
const tokenData = JSON.parse(
Buffer.from(tunnelToken, "base64").toString("utf8"),
);
const credentials = {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
AccountTag: tokenData.a || this.accountId,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
TunnelID: tokenData.t || tunnelId,
TunnelName: tunnelName,
TunnelSecret: tokenData.s,
};
await fs.writeFile(
this.credsPath,
JSON.stringify(credentials, null, 2),
"utf8",
);
log.info(`Created credentials file at: ${this.credsPath}`);
const tunnelConfig: CloudflaredConfig = {
tunnel: tunnelId,
"credentials-file": this.credsPath,
ingress: [
...Array.from(subdomainToService.entries()).map(
([subdomain, service]) => ({
hostname: `${subdomain}.${domain}`,
service: service,
}),
),
{
service: "http_status:404",
},
],
};
// Write config file
await fs.writeFile(this.configPath, yaml.dump(tunnelConfig), "utf8");
log.info(`Created config file at: ${this.configPath}`);
}
private async updateDNSRecord(
zoneId: string,
tunnelId: string,
subdomain: string,
domain: string,
): Promise<void> {
const existingRecords = await this.makeRequest<DNSRecordResponse>(
`${this.baseUrl}/zones/${zoneId}/dns_records?name=${subdomain}.${domain}`,
);
const recordId = existingRecords.result[0]?.id;
const dnsData = {
type: "CNAME",
name: subdomain,
content: `${tunnelId}.cfargotunnel.com`,
ttl: 1,
proxied: true,
};
if (recordId) {
log.info(`Updating existing DNS record for ${subdomain}.${domain}...`);
await this.makeRequest(
`${this.baseUrl}/zones/${zoneId}/dns_records/${recordId}`,
"PUT",
dnsData,
);
} else {
log.info(`Creating new DNS record for ${subdomain}.${domain}...`);
await this.makeRequest(
`${this.baseUrl}/zones/${zoneId}/dns_records`,
"POST",
dnsData,
);
}
}
public async startCloudflared() {
const cloudflared = spawn(
"cloudflared",
[
"tunnel",
"--config",
this.configPath,
"--loglevel",
"error",
"--protocol",
"http2",
"--retries",
"15",
"--no-autoupdate",
"run",
],
{
detached: true,
stdio: ["ignore", "pipe", "pipe"],
env: {
...process.env,
// Set this to bypass origin cert requirement for named tunnels
TUNNEL_ORIGIN_CERT: "/dev/null",
},
},
);
cloudflared.stdout?.on("data", (data) => {
log.info(data.toString().trim());
});
cloudflared.stderr?.on("data", (data) => {
log.error(data.toString().trim());
});
cloudflared.on("error", (error) => {
log.error("Failed to start cloudflared", {
error: error.message,
});
});
cloudflared.on("exit", (code, signal) => {
if (code !== null) {
log.error(`Cloudflared exited with code ${code}`, {
exitCode: code,
signal,
});
}
});
cloudflared.unref();
}
}
+8 -36
View File
@@ -1,11 +1,12 @@
import cluster from "cluster";
import crypto from "crypto";
import express from "express";
import rateLimit from "express-rate-limit";
import http from "http";
import path from "path";
import { fileURLToPath } from "url";
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
import { GameInfo, ID } from "../core/Schemas";
import { GameInfo } from "../core/Schemas";
import { generateID } from "../core/Util";
import { logger } from "./Logger";
import { MapPlaylist } from "./MapPlaylist";
@@ -73,10 +74,15 @@ export async function startMaster() {
log.info(`Primary ${process.pid} is running`);
log.info(`Setting up ${config.numWorkers()} workers...`);
// Generate admin token for worker authentication
const ADMIN_TOKEN = crypto.randomBytes(16).toString("hex");
process.env.ADMIN_TOKEN = ADMIN_TOKEN;
// Fork workers
for (let i = 0; i < config.numWorkers(); i++) {
const worker = cluster.fork({
WORKER_ID: i,
ADMIN_TOKEN,
});
log.info(`Started worker ${i} (PID: ${worker.process.pid})`);
@@ -128,6 +134,7 @@ export async function startMaster() {
// Restart the worker with the same ID
const newWorker = cluster.fork({
WORKER_ID: workerId,
ADMIN_TOKEN,
});
log.info(
@@ -154,41 +161,6 @@ app.get("/api/public_lobbies", async (req, res) => {
res.send(publicLobbiesJsonStr);
});
app.post("/api/kick_player/:gameID/:clientID", async (req, res) => {
if (req.headers[config.adminHeader()] !== config.adminToken()) {
res.status(401).send("Unauthorized");
return;
}
const { gameID, clientID } = req.params;
if (!ID.safeParse(gameID).success || !ID.safeParse(clientID).success) {
res.sendStatus(400);
return;
}
try {
const response = await fetch(
`http://localhost:${config.workerPort(gameID)}/api/kick_player/${gameID}/${clientID}`,
{
method: "POST",
headers: {
[config.adminHeader()]: config.adminToken(),
},
},
);
if (!response.ok) {
throw new Error(`Failed to kick player: ${response.statusText}`);
}
res.status(200).send("Player kicked successfully");
} catch (error) {
log.error(`Error kicking player from game ${gameID}:`, error);
res.status(500).send("Failed to kick player");
}
});
async function fetchLobbies(): Promise<number> {
const fetchPromises: Promise<GameInfo | null>[] = [];
-41
View File
@@ -1,22 +1,15 @@
import cluster from "cluster";
import * as dotenv from "dotenv";
import { GameEnv } from "../core/configuration/Config";
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
import { Cloudflare, TunnelConfig } from "./Cloudflare";
import { startMaster } from "./Master";
import { startWorker } from "./Worker";
// Load environment variables before we read configuration values derived from them.
dotenv.config();
const config = getServerConfigFromServer();
// Main entry point of the application
async function main() {
// Check if this is the primary (master) process
if (cluster.isPrimary) {
if (config.env() !== GameEnv.Dev) {
await setupTunnels();
}
console.log("Starting master process...");
await startMaster();
} else {
@@ -31,37 +24,3 @@ main().catch((error) => {
console.error("Failed to start server:", error);
process.exit(1);
});
async function setupTunnels() {
const cloudflare = new Cloudflare(
config.cloudflareAccountId(),
config.cloudflareApiToken(),
config.cloudflareConfigPath(),
config.cloudflareCredsPath(),
);
const domainToService = new Map<string, string>().set(
config.subdomain(),
// TODO: change to 3000 when we have a proper tunnel setup.
`http://localhost:80`,
);
for (let i = 0; i < config.numWorkers(); i++) {
domainToService.set(
`w${i}-${config.subdomain()}`,
`http://localhost:${3000 + i + 1}`,
);
}
if (!(await cloudflare.configAlreadyExists())) {
await cloudflare.createTunnel({
subdomain: config.subdomain(),
domain: config.domain(),
subdomainToService: domainToService,
} as TunnelConfig);
} else {
console.log("Config already exists, skipping tunnel creation");
}
await cloudflare.startCloudflared();
}
-18
View File
@@ -270,24 +270,6 @@ export async function startWorker() {
}
});
app.post("/api/kick_player/:gameID/:clientID", async (req, res) => {
if (req.headers[config.adminHeader()] !== config.adminToken()) {
res.status(401).send("Unauthorized");
return;
}
const { gameID, clientID } = req.params;
const game = gm.game(gameID);
if (!game) {
res.status(404).send("Game not found");
return;
}
game.kickClient(clientID);
res.status(200).send("Player kicked successfully");
});
// WebSocket handling
wss.on("connection", (ws: WebSocket, req) => {
ws.on("message", async (message: string) => {
+84
View File
@@ -1,5 +1,89 @@
#!/bin/bash
set -e
# Check if required environment variables are set
if [ -z "$CF_API_TOKEN" ] || [ -z "$CF_ACCOUNT_ID" ] || [ -z "$SUBDOMAIN" ] || [ -z "$DOMAIN" ]; then
echo "Error: Required environment variables not set"
echo "Please set CF_API_TOKEN, CF_ACCOUNT_ID, SUBDOMAIN, and DOMAIN"
exit 1
fi
# Generate a unique tunnel name using timestamp
TIMESTAMP=$(date +%Y%m%d%H%M%S)
TUNNEL_NAME="${SUBDOMAIN}-tunnel-${TIMESTAMP}"
echo "Using unique tunnel name: ${TUNNEL_NAME}"
# Create a new tunnel
echo "Creating Cloudflare tunnel for subdomain ${SUBDOMAIN}..."
TUNNEL_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/cfd_tunnel" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"name\":\"${TUNNEL_NAME}\"}")
# Extract tunnel ID and token
TUNNEL_ID=$(echo $TUNNEL_RESPONSE | jq -r '.result.id')
TUNNEL_TOKEN=$(echo $TUNNEL_RESPONSE | jq -r '.result.token')
if [ -z "$TUNNEL_ID" ] || [ "$TUNNEL_ID" == "null" ]; then
echo "Failed to create tunnel"
echo $TUNNEL_RESPONSE
exit 1
fi
echo "Tunnel created with ID: ${TUNNEL_ID}"
# Configure the tunnel with hostname
echo "Configuring tunnel to point to ${SUBDOMAIN}.${DOMAIN}..."
curl -s -X PUT "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/cfd_tunnel/${TUNNEL_ID}/configurations" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"config\":{\"ingress\":[{\"hostname\":\"${SUBDOMAIN}.${DOMAIN}\",\"service\":\"http://localhost:80\"},{\"service\":\"http_status:404\"}]}}"
# Update DNS record to point to the new tunnel
echo "Updating DNS record to point to the new tunnel..."
# First check if DNS record exists
DNS_RECORDS=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=${DOMAIN}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json")
ZONE_ID=$(echo $DNS_RECORDS | jq -r '.result[0].id')
if [ -z "$ZONE_ID" ] || [ "$ZONE_ID" == "null" ]; then
echo "Could not find zone ID for domain ${DOMAIN}"
exit 1
fi
# Check for existing record
EXISTING_RECORDS=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?name=${SUBDOMAIN}.${DOMAIN}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json")
RECORD_ID=$(echo $EXISTING_RECORDS | jq -r '.result[0].id')
# Create or update the DNS record
if [ -z "$RECORD_ID" ] || [ "$RECORD_ID" == "null" ]; then
# Create new record
echo "Creating new DNS record..."
DNS_RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"CNAME\",\"name\":\"${SUBDOMAIN}\",\"content\":\"${TUNNEL_ID}.cfargotunnel.com\",\"ttl\":1,\"proxied\":true}")
else
# Update existing record
echo "Updating existing DNS record..."
DNS_RESPONSE=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"CNAME\",\"name\":\"${SUBDOMAIN}\",\"content\":\"${TUNNEL_ID}.cfargotunnel.com\",\"ttl\":1,\"proxied\":true}")
fi
# Log the tunnel information
echo "Tunnel configuration is set up! Site will be available at: https://${SUBDOMAIN}.${DOMAIN}"
# Export the tunnel token for supervisord
export CLOUDFLARE_TUNNEL_TOKEN=${TUNNEL_TOKEN}
# Start supervisord
if [ "$DOMAIN" = openfront.dev ] && [ "$SUBDOMAIN" != main ]; then
exec timeout 18h /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
+9 -1
View File
@@ -22,4 +22,12 @@ user=node
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
stderr_logfile_maxbytes=0
[program:cloudflared]
command=cloudflared tunnel run --token %(ENV_CLOUDFLARE_TUNNEL_TOKEN)s
autostart=true
autorestart=true
user=node
stdout_logfile=/var/log/cloudflared.log
stderr_logfile=/var/log/cloudflared-err.log
-24
View File
@@ -22,24 +22,12 @@ export class TestServerConfig implements ServerConfig {
stripePublishableKey(): string {
throw new Error("Method not implemented.");
}
cloudflareConfigPath(): string {
throw new Error("Method not implemented.");
}
cloudflareCredsPath(): string {
throw new Error("Method not implemented.");
}
domain(): string {
throw new Error("Method not implemented.");
}
subdomain(): string {
throw new Error("Method not implemented.");
}
cloudflareAccountId(): string {
throw new Error("Method not implemented.");
}
cloudflareApiToken(): string {
throw new Error("Method not implemented.");
}
jwtAudience(): string {
throw new Error("Method not implemented.");
}
@@ -94,16 +82,4 @@ export class TestServerConfig implements ServerConfig {
gitCommit(): string {
throw new Error("Method not implemented.");
}
r2Bucket(): string {
throw new Error("Method not implemented.");
}
r2Endpoint(): string {
throw new Error("Method not implemented.");
}
r2AccessKey(): string {
throw new Error("Method not implemented.");
}
r2SecretKey(): string {
throw new Error("Method not implemented.");
}
}
+6 -6
View File
@@ -28,12 +28,12 @@ echo "======================================================"
# Container and image configuration
CONTAINER_NAME="openfront-${ENV}-${SUBDOMAIN}"
echo "Pulling ${DOCKER_IMAGE} from Docker Hub..."
docker pull "${DOCKER_IMAGE}"
echo "Pulling ${GHCR_IMAGE} from GitHub Container Registry..."
docker pull "${GHCR_IMAGE}"
echo "Checking for existing container..."
# Check for running container
RUNNING_CONTAINER="$(docker ps | grep ${CONTAINER_NAME} | awk '{print $1}')"
# Use docker ps with filter for exact name match
RUNNING_CONTAINER="$(docker ps --filter "name=^${CONTAINER_NAME}$" -q)"
if [ -n "$RUNNING_CONTAINER" ]; then
echo "Stopping running container $RUNNING_CONTAINER..."
docker stop "$RUNNING_CONTAINER"
@@ -44,7 +44,7 @@ if [ -n "$RUNNING_CONTAINER" ]; then
fi
# Also check for stopped containers with the same name
STOPPED_CONTAINER="$(docker ps -a | grep ${CONTAINER_NAME} | awk '{print $1}')"
STOPPED_CONTAINER="$(docker ps -a --filter "name=^${CONTAINER_NAME}$" -q)"
if [ -n "$STOPPED_CONTAINER" ]; then
echo "Removing stopped container $STOPPED_CONTAINER..."
docker rm "$STOPPED_CONTAINER"
@@ -67,7 +67,7 @@ docker run -d \
--env-file "$ENV_FILE" \
--name "${CONTAINER_NAME}" \
-v "cloudflared-${CONTAINER_NAME}:/etc/cloudflared" \
"${DOCKER_IMAGE}"
"${GHCR_IMAGE}"
if [ $? -eq 0 ]; then
echo "Update complete! New ${CONTAINER_NAME} container is running."