From 1714225d00993263f5ceec0b4dc738725e201bea Mon Sep 17 00:00:00 2001
From: badbl0cks <4161747+badbl0cks@users.noreply.github.com>
Date: Sun, 1 Feb 2026 13:14:32 -0800
Subject: [PATCH] Add CI/CD build and deploy scripts, along with
docker-compose, HAProxy config, and a certbot merge hook. Set up env.example
generation. Add doiuse dev dependency.
---
.env.example | 18 +++++
.forgejo/workflows/build-and-deploy.yml | 59 +++++++++++++++
bun.lockb | Bin 227174 -> 238804 bytes
cicd/scripts/build.sh | 29 +++++++
cicd/scripts/deploy.sh | 42 +++++++++++
deploy/certs/renewal-hooks/deploy/merge.sh | 4 +
deploy/docker-compose.yml | 84 +++++++++++++++++++++
deploy/haproxy.cfg | 43 +++++++++++
package.json | 1 +
src/pages/css-test.astro | 9 ++-
utils/generate-env-example.sh | 47 ++++++++++++
11 files changed, 334 insertions(+), 2 deletions(-)
create mode 100644 .env.example
create mode 100644 .forgejo/workflows/build-and-deploy.yml
create mode 100755 cicd/scripts/build.sh
create mode 100755 cicd/scripts/deploy.sh
create mode 100644 deploy/certs/renewal-hooks/deploy/merge.sh
create mode 100644 deploy/docker-compose.yml
create mode 100644 deploy/haproxy.cfg
create mode 100755 utils/generate-env-example.sh
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..93f995b
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,18 @@
+CERTBOT_EMAIL=${CERTBOT_EMAIL}
+CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
+DOMAIN=${DOMAIN}
+PUBLIC_IP=${PUBLIC_IP}
+ANDROID_SMS_GATEWAY_IP=${ANDROID_SMS_GATEWAY_IP}
+ANDROID_SMS_GATEWAY_URL=${ANDROID_SMS_GATEWAY_URL}
+ANDROID_SMS_GATEWAY_LOGIN=${ANDROID_SMS_GATEWAY_LOGIN}
+ANDROID_SMS_GATEWAY_PASSWORD=${ANDROID_SMS_GATEWAY_PASSWORD}
+ANDROID_SMS_GATEWAY_RECIPIENT_PHONE=${ANDROID_SMS_GATEWAY_RECIPIENT_PHONE}
+ASTRO_DB_REMOTE_URL=${ASTRO_DB_REMOTE_URL}
+OTP_SUPER_SECRET_SALT=${OTP_SUPER_SECRET_SALT}
+IMAGE_FILENAME=${IMAGE_FILENAME}
+IMAGE_NAME=${IMAGE_NAME}
+SSH_USER=${SSH_USER}
+SSH_PORT=${SSH_PORT}
+SSH_HOST=${SSH_HOST}
+SSH_KEY="${SSH_KEY}"
+SSH_KNOWN_HOST="${SSH_KNOWN_HOST}"
diff --git a/.forgejo/workflows/build-and-deploy.yml b/.forgejo/workflows/build-and-deploy.yml
new file mode 100644
index 0000000..7396a1f
--- /dev/null
+++ b/.forgejo/workflows/build-and-deploy.yml
@@ -0,0 +1,59 @@
+name: Build And Deploy
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ build-and-deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Install dependencies
+ run: |
+ apt-get update && apt-get install gettext -y
+ - name: Check out repository
+ uses: actions/checkout@v4
+ # - name: Expose repo secrets and vars as shell variables
+ # env:
+ # SECRETS_CONTEXT: ${{ toJSON(secrets) }}
+ # VARS_CONTEXT: ${{ toJSON(vars) }}
+ # run: |
+ # # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable
+ # # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
+ # # # EOF randomness is to account for empty secrets and vars
+ # EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
+ # to_envs() { jq -r "to_entries[] | \"\(.key)<<$EOF\n\(.value)\n$EOF\n\""; }
+ # echo "$VARS_CONTEXT" | to_envs >> $GITHUB_ENV
+ # echo "$SECRETS_CONTEXT" | to_envs >> $GITHUB_ENV
+ - name: Substitute environment variables in .env.example and write to .env
+ env:
+ CERTBOT_EMAIL: ${{secrets.CERTBOT_EMAIL}}
+ CLOUDFLARE_API_TOKEN: ${{secrets.CLOUDFLARE_API_TOKEN}}
+ DOMAIN: ${{secrets.DOMAIN}}
+ PUBLIC_IP: ${{secrets.PUBLIC_IP}}
+ ANDROID_SMS_GATEWAY_IP: ${{secrets.ANDROID_SMS_GATEWAY_IP}}
+ ANDROID_SMS_GATEWAY_URL: ${{secrets.ANDROID_SMS_GATEWAY_URL}}
+ ANDROID_SMS_GATEWAY_LOGIN: ${{secrets.ANDROID_SMS_GATEWAY_LOGIN}}
+ ANDROID_SMS_GATEWAY_PASSWORD: ${{secrets.ANDROID_SMS_GATEWAY_PASSWORD}}
+ ANDROID_SMS_GATEWAY_RECIPIENT_PHONE: ${{secrets.ANDROID_SMS_GATEWAY_RECIPIENT_PHONE}}
+ ASTRO_DB_REMOTE_URL: ${{secrets.ASTRO_DB_REMOTE_URL}}
+ OTP_SUPER_SECRET_SALT: ${{secrets.OTP_SUPER_SECRET_SALT}}
+ IMAGE_FILENAME: ${{secrets.IMAGE_FILENAME}}
+ IMAGE_NAME: ${{secrets.IMAGE_NAME}}
+ SSH_USER: ${{secrets.SSH_USER}}
+ SSH_PORT: ${{secrets.SSH_PORT}}
+ SSH_HOST: ${{secrets.SSH_HOST}}
+ SSH_KEY: ${{secrets.SSH_KEY}}
+ SSH_KNOWN_HOST: ${{secrets.SSH_KNOWN_HOST}}
+ run: |
+ envsubst < .env.example > .env
+ - name: Run build script
+ run: |
+ cd cicd/scripts
+ chmod +x ./build.sh
+ ./build.sh
+ - name: Run deploy script
+ run: |
+ cd cicd/scripts
+ chmod +x ./deploy.sh
+ ./deploy.sh
diff --git a/bun.lockb b/bun.lockb
index 2656e95f5c885dacca2d56136e574d3aba4e3571..0cc7456c869a9d9b52e6ede651be4c1453e145ca 100755
GIT binary patch
delta 17860
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
delta 10747
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
diff --git a/cicd/scripts/build.sh b/cicd/scripts/build.sh
new file mode 100755
index 0000000..3dedac4
--- /dev/null
+++ b/cicd/scripts/build.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+#######################
+# VARIABLES #
+#######################
+ROOT_DIR=$(dirname $(dirname $(dirname $(realpath $0))))
+GIT_REF=${GIT_REF:-main}
+
+### NO EDITS BELOW THIS LINE ###
+cd ${ROOT_DIR}
+source .env
+git checkout ${GIT_REF}
+GIT_SHA=$(git rev-parse --short HEAD)
+
+if [[ "${GIT_REF}" =~ ^refs/tags/v([0-9]+\.[0-9]+\.[0-9]+)(-.*)?$ ]]; then
+ VERSION="${BASH_REMATCH[1]}"
+ if [[ -n "${BASH_REMATCH[2]}" ]]; then
+ VERSION="${VERSION}${BASH_REMATCH[2]}"
+ fi
+ echo "Using git tag version: ${VERSION}"
+else
+ VERSION=$(node -p "require('./package.json').version || '0.0.0'")
+ GIT_SHA_SHORT="${GIT_SHA:0:7}"
+ VERSION="${VERSION}-${GIT_SHA_SHORT}"
+ echo "Using package.json + SHA version: ${VERSION}"
+fi
+
+docker build -t ${IMAGE_NAME}:latest -t ${IMAGE_NAME}:v${VERSION} --build-arg VERSION=${VERSION} .
+docker save -o ${IMAGE_FILENAME} ${IMAGE_NAME}:latest
diff --git a/cicd/scripts/deploy.sh b/cicd/scripts/deploy.sh
new file mode 100755
index 0000000..350fa0a
--- /dev/null
+++ b/cicd/scripts/deploy.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+#######################
+# VARIABLES #
+#######################
+ROOT_DIR=$(dirname $(dirname $(dirname $(realpath $0))))
+
+### NO EDITS BELOW THIS LINE ###
+cd ${ROOT_DIR}
+source .env
+
+mkdir -p ${HOME}/.ssh
+chmod 700 ${HOME}/.ssh
+echo "${SSH_KEY}" > ${HOME}/.ssh/id_ed25519-${SSH_HOST//./_}
+echo "${SSH_KNOWN_HOST}" > ${HOME}/.ssh/known_hosts-${SSH_HOST//./_}
+chmod -R 600 ${HOME}/.ssh/
+chmod 700 ${HOME}/.ssh
+
+grep -q "Host ${SSH_HOST}" ${HOME}/.ssh/config || cat >> ${HOME}/.ssh/config < /etc/letsencrypt/fullcert.pem
+chmod 755 /etc/letsencrypt/
+chmod 644 /etc/letsencrypt/fullcert.pem
diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml
new file mode 100644
index 0000000..b5dd6a4
--- /dev/null
+++ b/deploy/docker-compose.yml
@@ -0,0 +1,84 @@
+services:
+ badblocks-personal-site:
+ image: ${IMAGE_NAME}:latest
+ restart: always
+ container_name: badblocks-personal-site
+ ports:
+ - "4321:4321"
+ networks:
+ - proxynet
+ env_file:
+ - .env
+ # healthcheck:
+ # test:
+ # [
+ # "CMD",
+ # "curl",
+ # "-f",
+ # "-s",
+ # "--max-time",
+ # "5",
+ # "http://localhost:4321/health",
+ # ]
+ # interval: 30s
+ # timeout: 15s
+ # retries: 3
+ # start_period: 120s
+ # wireguard:
+ # image: qmcgaw/gluetun
+ # cap_add:
+ # - NET_ADMIN
+ # container_name: wireguard
+ # environment:
+ # - VPN_SERVICE_PROVIDER=custom
+ # - VPN_TYPE=wireguard
+ # - HTTPPROXY=on
+ # expose:
+ # - "8888"
+ # env_file:
+ # - .env
+ # devices:
+ # - /dev/net/tun:/dev/net/tun
+ # restart: unless-stopped
+ # networks:
+ # - proxynet
+ # healthcheck:
+ # test: ss["CMD", "ping", "-c", "1", "-W", "3", "$$ANDROID_SMS_GATEWAY_IP"]
+ # interval: 30s
+ # timeout: 15s
+ # retries: 3
+ # start_period: 60s
+ certbot:
+ image: serversideup/certbot-dns-cloudflare
+ volumes:
+ - ./certs:/etc/letsencrypt
+ environment:
+ CLOUDFLARE_API_TOKEN: "${CLOUDFLARE_API_TOKEN}"
+ CERTBOT_EMAIL: "${CERTBOT_EMAIL}"
+ CERTBOT_DOMAINS: "${DOMAIN}"
+ haproxy:
+ image: haproxy:3.2
+ stop_signal: SIGTERM
+ container_name: haproxy
+ env_file:
+ - .env
+ command: ["haproxy", "-f", "/usr/local/etc/haproxy"]
+ ports:
+ - "${PUBLIC_IP}:80:80"
+ - "${PUBLIC_IP}:443:443"
+ - "${PUBLIC_IP}:8404:8404"
+ volumes:
+ - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
+ - ./certs:/certs:ro
+ restart: always
+ networks:
+ - proxynet
+ # healthcheck:
+ # test: ["CMD", "haproxy", "-c", "-f", "/usr/local/etc/haproxy"]
+ # interval: 30s
+ # timeout: 10s
+ # retries: 3
+networks:
+ proxynet:
+ name: proxynet
+ driver: bridge
diff --git a/deploy/haproxy.cfg b/deploy/haproxy.cfg
new file mode 100644
index 0000000..4e7fc5e
--- /dev/null
+++ b/deploy/haproxy.cfg
@@ -0,0 +1,43 @@
+global
+ daemon
+ log stdout format raw local0 info
+ maxconn 2000
+
+defaults
+ mode http
+ log global
+ timeout connect 5s
+ timeout client 30s
+ timeout server 30s
+ timeout check 5s
+ retries 3
+ option httplog
+ option dontlognull
+ option redispatch
+
+frontend http
+ bind :80
+ mode http
+
+ http-request redirect scheme https unless { ssl_fc }
+
+frontend https
+ bind :443 ssl crt /certs/fullcert.pem
+
+ http-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains; preload;"
+ default_backend main
+
+backend main
+ balance leastconn
+ option httpchk GET /
+ http-check expect status 200
+
+ server badblocks-personal-site badblocks-personal-site:4321 check resolvers docker resolve-prefer ipv4 init-addr none
+
+resolvers docker
+ nameserver dns1 127.0.0.11:53
+ resolve_retries 3
+ timeout resolve 1s
+ timeout retry 1s
+ hold valid 10s
+ hold obsolete 30s
diff --git a/package.json b/package.json
index 804ecd6..ea5cbff 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"devDependencies": {
"@types/bun": "^1.3.6",
"@types/validator": "^13.15.10",
+ "doiuse": "^6.0.6",
"prettier": "^3.8.1",
"prettier-plugin-astro": "^0.14.1"
}
diff --git a/src/pages/css-test.astro b/src/pages/css-test.astro
index 44fd0fc..b68fba3 100644
--- a/src/pages/css-test.astro
+++ b/src/pages/css-test.astro
@@ -469,7 +469,8 @@
audio
+ />audio
@@ -705,7 +706,11 @@
Datetime input
-
+
Datetime-local input
diff --git a/utils/generate-env-example.sh b/utils/generate-env-example.sh
new file mode 100755
index 0000000..e1afeec
--- /dev/null
+++ b/utils/generate-env-example.sh
@@ -0,0 +1,47 @@
+#!/bin/bash
+
+cd $(dirname $(dirname $(realpath $0)))
+
+# Path to the original .env file
+ENV_FILE=".env"
+# Path to the new .env.example file
+EXAMPLE_FILE=".env.example"
+
+# Check if the .env file exists
+if [ ! -f "$ENV_FILE" ]; then
+ echo "The file $ENV_FILE does not exist."
+ exit 1
+fi
+
+# Create or empty the .env.example file
+> "$EXAMPLE_FILE"
+
+SKIP_NEXT=false
+
+# Read each line in .env
+while IFS= read -r line; do
+ # Skip the current line if the previous line is part of a multiline/quoted string
+ if [[ $SKIP_NEXT == true ]]; then
+ if [[ $line == *'"'* ]]; then
+ SKIP_NEXT=false
+ fi
+ continue
+ # Copy comments and empty lines verbatim
+ elif [[ $line == \#* ]] || [[ -z $line ]]; then
+ echo "$line" >> "$EXAMPLE_FILE"
+ continue
+ # Check if the line is a multiline/quoted string
+ elif [[ $line == *'="'* ]]; then
+ if [[ $line != *'"' ]]; then
+ SKIP_NEXT=true
+ fi
+ LINE=${line%%=*}
+ echo "$LINE=\"\${$LINE}\"" >> "$EXAMPLE_FILE"
+ # For all other lines, copy only the key (everything before the '=') if present
+ elif [[ $line == *'='* ]]; then
+ LINE=${line%%=*}
+ echo "$LINE=\${$LINE}" >> "$EXAMPLE_FILE"
+ fi
+done < "$ENV_FILE"
+
+echo ".env.example file created successfully."