283 lines
12 KiB
YAML
283 lines
12 KiB
YAML
name: "Deploy scripts to remote server"
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
ref:
|
|
description: "Branch/tag/SHA for deployment"
|
|
required: false
|
|
default: "master"
|
|
|
|
jobs:
|
|
deploy:
|
|
name: "Upload and deploy on 192.168.10.14"
|
|
runs-on: native
|
|
|
|
steps:
|
|
- name: "Checkout repository"
|
|
shell: bash
|
|
env:
|
|
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
|
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
TOKEN="${GIT_TOKEN:-$GITEA_TOKEN}"
|
|
if [ -z "${TOKEN}" ]; then
|
|
echo "ERROR: GIT_TOKEN (or GITEA_TOKEN) secret is required"
|
|
exit 1
|
|
fi
|
|
|
|
REF="${{ github.event.inputs.ref || 'master' }}"
|
|
SERVER="${{ github.server_url }}"
|
|
REPO="${{ github.repository }}"
|
|
HOST="${SERVER#https://}"
|
|
HOST="${HOST#http://}"
|
|
|
|
if [ "${HOST}" = "github.com" ]; then
|
|
CLONE_URL="https://x-access-token:${TOKEN}@${HOST}/${REPO}.git"
|
|
else
|
|
CLONE_URL="https://${TOKEN}@${HOST}/${REPO}.git"
|
|
fi
|
|
|
|
git clone --branch "${REF}" --depth 1 "${CLONE_URL}" . 2>&1 | grep -F -v "${TOKEN}" || true
|
|
echo ">>> Source checked out: $(git log --oneline -1)"
|
|
|
|
- name: "Validate deployment secrets"
|
|
shell: bash
|
|
env:
|
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
|
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
|
|
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
|
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
|
|
DEPLOY_SERVICE: ${{ secrets.DEPLOY_SERVICE }}
|
|
DEPLOY_SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
|
|
DEPLOY_SSH_KNOWN_HOSTS: ${{ secrets.DEPLOY_SSH_KNOWN_HOSTS }}
|
|
CFG_REGRU_USERNAME: ${{ secrets.CFG_REGRU_USERNAME }}
|
|
CFG_REGRU_PASSWORD: ${{ secrets.CFG_REGRU_PASSWORD }}
|
|
CFG_EMAIL: ${{ secrets.CFG_EMAIL }}
|
|
CFG_DOMAINS: ${{ secrets.CFG_DOMAINS }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
: "${DEPLOY_HOST:?DEPLOY_HOST secret is required}"
|
|
: "${DEPLOY_PORT:?DEPLOY_PORT secret is required}"
|
|
: "${DEPLOY_USER:?DEPLOY_USER secret is required}"
|
|
: "${DEPLOY_PATH:?DEPLOY_PATH secret is required}"
|
|
: "${DEPLOY_SERVICE:?DEPLOY_SERVICE secret is required}"
|
|
: "${DEPLOY_SSH_PRIVATE_KEY:?DEPLOY_SSH_PRIVATE_KEY secret is required}"
|
|
: "${CFG_REGRU_USERNAME:?CFG_REGRU_USERNAME secret is required}"
|
|
: "${CFG_REGRU_PASSWORD:?CFG_REGRU_PASSWORD secret is required}"
|
|
: "${CFG_EMAIL:?CFG_EMAIL secret is required}"
|
|
: "${CFG_DOMAINS:?CFG_DOMAINS secret is required}"
|
|
|
|
if [ "${DEPLOY_HOST}" != "192.168.10.14" ]; then
|
|
echo "ERROR: DEPLOY_HOST must be 192.168.10.14 for this workflow"
|
|
exit 1
|
|
fi
|
|
|
|
echo ">>> Secrets validation passed"
|
|
|
|
- name: "Create deployment archive"
|
|
shell: bash
|
|
env:
|
|
CFG_REGRU_USERNAME: ${{ secrets.CFG_REGRU_USERNAME }}
|
|
CFG_REGRU_PASSWORD: ${{ secrets.CFG_REGRU_PASSWORD }}
|
|
CFG_DOMAIN: ${{ secrets.CFG_DOMAIN }}
|
|
CFG_DOMAINS: ${{ secrets.CFG_DOMAINS }}
|
|
CFG_WILDCARD: ${{ secrets.CFG_WILDCARD }}
|
|
CFG_EMAIL: ${{ secrets.CFG_EMAIL }}
|
|
CFG_CERT_DIR: ${{ secrets.CFG_CERT_DIR }}
|
|
CFG_LOG_FILE: ${{ secrets.CFG_LOG_FILE }}
|
|
CFG_DNS_PROPAGATION_WAIT: ${{ secrets.CFG_DNS_PROPAGATION_WAIT }}
|
|
CFG_DNS_CHECK_ATTEMPTS: ${{ secrets.CFG_DNS_CHECK_ATTEMPTS }}
|
|
CFG_DNS_CHECK_INTERVAL: ${{ secrets.CFG_DNS_CHECK_INTERVAL }}
|
|
CFG_RENEWAL_DAYS: ${{ secrets.CFG_RENEWAL_DAYS }}
|
|
CFG_NPM_ENABLED: ${{ secrets.CFG_NPM_ENABLED }}
|
|
CFG_NPM_HOST: ${{ secrets.CFG_NPM_HOST }}
|
|
CFG_NPM_EMAIL: ${{ secrets.CFG_NPM_EMAIL }}
|
|
CFG_NPM_PASSWORD: ${{ secrets.CFG_NPM_PASSWORD }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
rm -rf .deploy-artifacts
|
|
mkdir -p .deploy-artifacts
|
|
|
|
if command -v rsync >/dev/null 2>&1; then
|
|
rsync -a \
|
|
--exclude '.git/' \
|
|
--exclude '.src-repo/' \
|
|
--exclude '.wiki-repo/' \
|
|
--exclude '.deploy-artifacts/' \
|
|
./ .deploy-artifacts/repo/
|
|
else
|
|
cp -a . .deploy-artifacts/repo
|
|
rm -rf .deploy-artifacts/repo/.git .deploy-artifacts/repo/.src-repo .deploy-artifacts/repo/.wiki-repo .deploy-artifacts/repo/.deploy-artifacts
|
|
fi
|
|
|
|
python3 - <<'PY'
|
|
import json
|
|
import os
|
|
|
|
|
|
def to_bool(value, default=False):
|
|
if value is None or value == "":
|
|
return default
|
|
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
|
|
|
|
|
def to_int(value, default):
|
|
if value is None or value == "":
|
|
return default
|
|
return int(value)
|
|
|
|
|
|
domains_raw = os.environ.get("CFG_DOMAINS", "").strip()
|
|
if domains_raw.startswith("["):
|
|
# JSON array: каждый элемент может содержать запятые для SAN групп
|
|
# Пример: ["*.dfv24.com,dfv24.com", "github.dfv24.com"]
|
|
domains = json.loads(domains_raw)
|
|
else:
|
|
domains = [item.strip() for item in domains_raw.split(",") if item.strip()]
|
|
|
|
if not domains:
|
|
raise RuntimeError("CFG_DOMAINS must contain at least one domain")
|
|
|
|
# primary domain: CFG_DOMAIN → первый не-wildcard домен из первой SAN группы
|
|
first_group_domains = [d.strip() for d in domains[0].split(",") if d.strip()]
|
|
domain = os.environ.get("CFG_DOMAIN", "").strip() or next(
|
|
(d for d in first_group_domains if not d.startswith("*.")),
|
|
first_group_domains[0]
|
|
)
|
|
|
|
config = {
|
|
"regru_username": os.environ["CFG_REGRU_USERNAME"],
|
|
"regru_password": os.environ["CFG_REGRU_PASSWORD"],
|
|
"domain": domain,
|
|
"domains": domains,
|
|
"wildcard": to_bool(os.environ.get("CFG_WILDCARD"), True),
|
|
"email": os.environ["CFG_EMAIL"],
|
|
"cert_dir": os.environ.get("CFG_CERT_DIR") or "/etc/letsencrypt/live",
|
|
"log_file": os.environ.get("CFG_LOG_FILE") or "/var/log/letsencrypt_regru.log",
|
|
"dns_propagation_wait": to_int(os.environ.get("CFG_DNS_PROPAGATION_WAIT"), 60),
|
|
"dns_check_attempts": to_int(os.environ.get("CFG_DNS_CHECK_ATTEMPTS"), 10),
|
|
"dns_check_interval": to_int(os.environ.get("CFG_DNS_CHECK_INTERVAL"), 10),
|
|
"renewal_days": to_int(os.environ.get("CFG_RENEWAL_DAYS"), 30),
|
|
"npm_enabled": to_bool(os.environ.get("CFG_NPM_ENABLED"), True),
|
|
"npm_host": os.environ.get("CFG_NPM_HOST") or "",
|
|
"npm_email": os.environ.get("CFG_NPM_EMAIL") or "",
|
|
"npm_password": os.environ.get("CFG_NPM_PASSWORD") or "",
|
|
}
|
|
|
|
output_path = os.path.join(".deploy-artifacts", "repo", "config.json")
|
|
with open(output_path, "w", encoding="utf-8") as file:
|
|
json.dump(config, file, ensure_ascii=False, indent=4)
|
|
PY
|
|
|
|
tar -czf .deploy-artifacts/deploy.tar.gz -C .deploy-artifacts repo
|
|
ls -lh .deploy-artifacts/deploy.tar.gz
|
|
|
|
- name: "Configure SSH client"
|
|
shell: bash
|
|
env:
|
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
|
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
|
|
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
|
DEPLOY_SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
|
|
DEPLOY_SSH_KNOWN_HOSTS: ${{ secrets.DEPLOY_SSH_KNOWN_HOSTS }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
mkdir -p ~/.ssh
|
|
chmod 700 ~/.ssh
|
|
|
|
# Save private key preserving exact formatting
|
|
# Use printf with %b to handle \n sequences, strip trailing whitespace issues
|
|
echo "${DEPLOY_SSH_PRIVATE_KEY}" | sed 's/\r//' > ~/.ssh/id_deploy
|
|
# Ensure file ends with a newline (required by OpenSSH)
|
|
echo "" >> ~/.ssh/id_deploy
|
|
chmod 600 ~/.ssh/id_deploy
|
|
|
|
# Validate key and show fingerprint for debugging
|
|
echo ">>> Private key type: $(ssh-keygen -l -f ~/.ssh/id_deploy 2>&1 || echo 'INVALID KEY')"
|
|
|
|
if [ -n "${DEPLOY_SSH_KNOWN_HOSTS:-}" ]; then
|
|
printf '%s\n' "${DEPLOY_SSH_KNOWN_HOSTS}" > ~/.ssh/known_hosts
|
|
echo ">>> known_hosts: loaded from DEPLOY_SSH_KNOWN_HOSTS secret"
|
|
fi
|
|
|
|
# Always run ssh-keyscan to ensure all key types are covered
|
|
echo ">>> Scanning host keys for ${DEPLOY_HOST}:${DEPLOY_PORT}..."
|
|
ssh-keyscan -p "${DEPLOY_PORT}" -t ed25519,ecdsa-sha2-nistp256,rsa "${DEPLOY_HOST}" >> ~/.ssh/known_hosts 2>&1 || true
|
|
echo ">>> known_hosts entries: $(wc -l < ~/.ssh/known_hosts)"
|
|
|
|
chmod 644 ~/.ssh/known_hosts
|
|
|
|
{
|
|
echo "Host deploy-target"
|
|
echo " HostName ${DEPLOY_HOST}"
|
|
echo " Port ${DEPLOY_PORT}"
|
|
echo " User ${DEPLOY_USER}"
|
|
echo " IdentityFile ~/.ssh/id_deploy"
|
|
echo " UserKnownHostsFile ~/.ssh/known_hosts"
|
|
echo " StrictHostKeyChecking accept-new"
|
|
echo " ConnectTimeout 15"
|
|
echo " ServerAliveInterval 10"
|
|
echo " ServerAliveCountMax 3"
|
|
echo " BatchMode yes"
|
|
} > ~/.ssh/config
|
|
chmod 600 ~/.ssh/config
|
|
|
|
echo ">>> Testing SSH connection..."
|
|
ssh deploy-target "echo '>>> SSH connection ok:' \"\$(hostname)\""
|
|
echo ">>> SSH configured successfully"
|
|
|
|
- name: "Upload deployment archive"
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
scp .deploy-artifacts/deploy.tar.gz deploy-target:/tmp/configure_nginx_manager_deploy.tar.gz
|
|
|
|
- name: "Deploy and restart remote service"
|
|
shell: bash
|
|
env:
|
|
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
|
|
DEPLOY_SERVICE: ${{ secrets.DEPLOY_SERVICE }}
|
|
DEPLOY_CONFIG_PATH: ${{ secrets.DEPLOY_CONFIG_PATH }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# DEPLOY_CONFIG_PATH: path to config.json on remote (default: /etc/letsencrypt-regru/config.json)
|
|
CONFIG_DEST="${DEPLOY_CONFIG_PATH:-/etc/letsencrypt-regru/config.json}"
|
|
|
|
ssh deploy-target "set -euo pipefail; \
|
|
TMP_DIR=\"/tmp/configure_nginx_manager_deploy\"; \
|
|
sudo rm -rf \"\${TMP_DIR}\"; \
|
|
sudo mkdir -p \"\${TMP_DIR}\"; \
|
|
sudo tar -xzf /tmp/configure_nginx_manager_deploy.tar.gz -C \"\${TMP_DIR}\"; \
|
|
sudo mkdir -p \"${DEPLOY_PATH}\"; \
|
|
sudo find \"${DEPLOY_PATH}\" -mindepth 1 -maxdepth 1 ! -name 'venv' -exec rm -rf {} + 2>/dev/null || true; \
|
|
sudo cp -a \"\${TMP_DIR}/repo/.\" \"${DEPLOY_PATH}/\"; \
|
|
if [ ! -d \"${DEPLOY_PATH}/venv\" ]; then \
|
|
echo \">>> venv not found, creating...\"; \
|
|
sudo python3 -m venv \"${DEPLOY_PATH}/venv\"; \
|
|
fi; \
|
|
echo \">>> Installing/updating Python dependencies...\"; \
|
|
sudo \"${DEPLOY_PATH}/venv/bin/pip\" install -q -r \"${DEPLOY_PATH}/requirements.txt\" 2>&1 | tail -5; \
|
|
sudo mkdir -p \"\$(dirname '${CONFIG_DEST}')\"; \
|
|
sudo cp -f \"\${TMP_DIR}/repo/config.json\" \"${CONFIG_DEST}\"; \
|
|
sudo chmod 600 \"${CONFIG_DEST}\"; \
|
|
echo \">>> config.json deployed to: ${CONFIG_DEST}\"; \
|
|
sudo rm -rf \"\${TMP_DIR}\" /tmp/configure_nginx_manager_deploy.tar.gz; \
|
|
sudo systemctl daemon-reload; \
|
|
sudo systemctl enable \"${DEPLOY_SERVICE}\" || true; \
|
|
echo \">>> Starting service...\"; \
|
|
sudo systemctl restart --no-block \"${DEPLOY_SERVICE}\" || true; \
|
|
sleep 3; \
|
|
echo \">>> Service status:\"; \
|
|
sudo systemctl --no-pager --full status \"${DEPLOY_SERVICE}\" | head -n 30 || true; \
|
|
echo \">>> Last journal entries:\"; \
|
|
sudo journalctl -u \"${DEPLOY_SERVICE}\" -n 30 --no-pager || true; \
|
|
echo \">>> Config used:\"; \
|
|
sudo cat \"${CONFIG_DEST}\" | python3 -c \"import json,sys; c=json.load(sys.stdin); c['regru_password']='***'; c['npm_password']='***'; print(json.dumps(c,indent=2,ensure_ascii=False))\" || true"
|