name: Nightly build & deploy # Rebuilds the site (refreshing the Discord embed's "today" snapshot), uploads # the built dist/ to the CMU AFS space over SFTP, then triggers CMU's publish # CGI so the files go live. on: # Midnight run. NOTE: cron here is UTC. 00:00 UTC is 8pm/7pm Eastern. # For midnight *Eastern*, use "0 5 * * *" (EST) or "0 4 * * *" (EDT). schedule: #- cron: "0 5 * * *" - cron: "*/10 * * * *" # Allow manual runs from the Gitea Actions UI. workflow_dispatch: {} # Also deploy on pushes to the main branch. push: branches: - main jobs: build-and-deploy: # Pinned to the onsite CMU runner. Only a runner registered with the # "cmu-onsite" label can reach linux.andrew.cmu.edu, so this guarantees the # deploy runs from inside CMU's network. A cloud/public runner will never # pick up this job. The runner must be registered in Docker mode so this # label maps to a Node-capable Debian image (see README). runs-on: cmu-onsite env: # Absolute base URL where the site is served. Used to build the absolute # icon URL embedded for Discord. Set SITE_BASE_URL as a secret/variable, # e.g. https://www.andrew.cmu.edu/user/ SITE_BASE_URL: ${{ secrets.SITE_BASE_URL }} steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Node uses: actions/setup-node@v4 with: node-version: "20" cache: "npm" - name: Install dependencies run: npm ci - name: Build (injects today's Red-plan embed) env: # Discord fetches this absolute URL for the embed thumbnail. SITE_ICON_URL: "${{ secrets.SITE_BASE_URL }}/apple-touch-icon.png" run: npm run build - name: Install lftp # The node:20-bookworm container runs as root and has no `sudo`, so call # apt-get directly. openssh-client provides the `ssh` binary lftp uses # for its SFTP connect-program. run: | apt-get update apt-get install -y --no-install-recommends lftp openssh-client - name: Preflight — check SFTP host reachability env: SFTP_HOST: ${{ secrets.SFTP_HOST }} SFTP_PORT: ${{ secrets.SFTP_PORT }} run: | set -uo pipefail PORT="${SFTP_PORT:-22}" if [ -z "${SFTP_HOST}" ]; then echo "::error::SFTP_HOST secret is empty. Set it in Gitea → Settings → Actions → Secrets." exit 1 fi echo "Testing TCP reachability of ${SFTP_HOST}:${PORT} ..." if ! timeout 15 bash -c "cat < /dev/null > /dev/tcp/${SFTP_HOST}/${PORT}" 2>/dev/null; then echo "::error::Cannot open a TCP connection to ${SFTP_HOST}:${PORT}. This is why lftp reports 'max-retries exceeded'." echo "Likely causes: wrong SFTP_HOST, wrong port, or the runner cannot reach CMU (needs VPN / on-campus network / firewall allowance)." exit 1 fi echo "Host is reachable on ${PORT}. Showing the SSH banner:" # Non-fatal: just prints what the server advertises (SSH version, etc.) timeout 10 bash -c "exec 3<>/dev/tcp/${SFTP_HOST}/${PORT}; head -c 200 <&3" 2>/dev/null || true echo - name: Upload dist/ via SFTP env: SFTP_HOST: ${{ secrets.SFTP_HOST }} SFTP_PORT: ${{ secrets.SFTP_PORT }} SFTP_USERNAME: ${{ secrets.SFTP_USERNAME }} SFTP_PASSWORD: ${{ secrets.SFTP_PASSWORD }} SFTP_REMOTE_PATH: ${{ secrets.SFTP_REMOTE_PATH }} run: | set -euo pipefail PORT="${SFTP_PORT:-22}" # lftp drives SFTP by shelling out to the system `ssh`. We pass ssh # options explicitly so this works on a fresh runner against CMU: # - StrictHostKeyChecking/UserKnownHostsFile: don't fail on an # unknown host key (the runner has never seen CMU's key). # - PubkeyAuthentication=no / PreferredAuthentications=password: # force password auth so ssh doesn't stall trying keys/agent. # - the KexAlgorithms/HostKeyAlgorithms "+..." lines re-enable # algorithms some CMU hosts still use that modern OpenSSH disables # by default (a common cause of a silent connection failure). # # To debug connection problems, temporarily add `debug 3;` before the # open line and `--verbose` to the mirror — both are omitted here to # keep the job log small (large logs slow the "Complete job" upload # back to the off-campus Gitea). lftp -c " set sftp:auto-confirm yes; set sftp:connect-program 'ssh -a -x \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o PubkeyAuthentication=no \ -o PreferredAuthentications=password \ -o KexAlgorithms=+diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1 \ -o HostKeyAlgorithms=+ssh-rsa,ssh-dss'; set net:max-retries 2; set net:timeout 30; set net:reconnect-interval-base 5; open -u '${SFTP_USERNAME}','${SFTP_PASSWORD}' sftp://${SFTP_HOST}:${PORT}; mkdir -f -p '${SFTP_REMOTE_PATH}'; mirror -R --delete --only-newer ./dist/ '${SFTP_REMOTE_PATH}'; bye " - name: Trigger CMU publish env: SFTP_USERNAME: ${{ secrets.SFTP_USERNAME }} run: | set -euo pipefail # FLAG=0 publishes; NAME is the AFS username (same as the SFTP user). # --fail makes curl exit non-zero on HTTP errors so the job fails # loudly if publishing is rejected. curl --fail --show-error --silent \ "https://www.andrew.cmu.edu/cgi-bin/publish?FLAG=0&NAME=${SFTP_USERNAME}" echo "Publish request sent for ${SFTP_USERNAME}."