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"