Continuous Deployment
Continuous Integration is used to run a deploy script for each web site hosted by the server, which gives me Continuous Deployment.
GitHub Actions
Section titled “GitHub Actions”The code for my sites is hosted on GitHub, where I can use GitHub Actions for Continuous Integration.
GitHub Actions is used to run a workflow when the site is updated.
The workflow that runs when this site is updated is:
name: Deploy
on: push: branches: [ main ] workflow_dispatch:
permissions: contents: read pages: write id-token: write
concurrency: group: "deploy" cancel-in-progress: false
jobs: build: runs-on: ubuntu-latest steps: - name: Checkout your repository using git uses: actions/checkout@v4 - name: Install & build uses: withastro/action@v3 with: path: ./docs
- name: Upload artifact uses: actions/upload-artifact@v4 with: name: astro-dist path: ./docs/dist retention-days: 7
deploy: needs: build runs-on: ubuntu-latest environment: name: pi url: https://infrastructure.paultibbetts.uk steps: - name: Checkout uses: actions/checkout@v4
- name: Download build artifact uses: actions/download-artifact@v4 with: name: astro-dist path: ./dist
- name: Write SSH key run: | install -m 700 -d ~/.ssh echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519
- name: Add server to known hosts run: | ssh-keyscan -H -p "${{ secrets.DEPLOY_PORT }}" "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts
- name: Deploy to server env: DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }} DEPLOY_USER: ${{ secrets.DEPLOY_USER }} DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} LOCAL_BUILD_DIR: ./dist SSH_KEY_PATH: ~/.ssh/id_ed25519 SSH_KNOWN_HOSTS_PATH: ~/.ssh/known_hosts KEEP_RELEASES: "3" run: | chmod +x ./docs/scripts/deploy.sh ./docs/scripts/deploy.shwhich builds the site and runs docs/scripts/deploy.sh whenever the main branch is updated.
Deploy script
Section titled “Deploy script”A shell script deploys the generated site to the web server:
#!/usr/bin/env bashset -euo pipefail
# ------------------------------------------------------------------------------# Deploy a pre-built static site to a host using an# Ansible deploy_helper-style layout:# <BASE>/releases/<TIMESTAMP>/# <BASE>/current -> releases/<TIMESTAMP>## Intended to run from CI.# ------------------------------------------------------------------------------
: "${DEPLOY_HOST:?Set DEPLOY_HOST (e.g. example.com)}": "${DEPLOY_USER:?Set DEPLOY_USER (e.g. deploy)}": "${DEPLOY_PATH:?Set DEPLOY_PATH (e.g. /srv/www/website)}": "${LOCAL_BUILD_DIR:?Set LOCAL_BUILD_DIR (e.g. ./public)}"
DEPLOY_PORT="${DEPLOY_PORT:-22}"KEEP_RELEASES="${KEEP_RELEASES:-5}"
RELEASE_ID="${RELEASE_ID:-$(date +%Y%m%dT%H%M%S)}"
RSYNC_FLAGS="${RSYNC_FLAGS:--az --delete --delay-updates --compress --human-readable}"
SSH_KEY_PATH="${SSH_KEY_PATH:-}"SSH_KNOWN_HOSTS_PATH="${SSH_KNOWN_HOSTS_PATH:-}"
if [[ ! -d "$LOCAL_BUILD_DIR" ]]; then echo "LOCAL_BUILD_DIR does not exist or is not a directory: $LOCAL_BUILD_DIR" >&2 exit 1fi
ssh_opts=( -p "$DEPLOY_PORT" -o BatchMode=yes -o StrictHostKeyChecking=yes)
if [[ -n "$SSH_KEY_PATH" ]]; then ssh_opts+=(-i "$SSH_KEY_PATH")fi
if [[ -n "$SSH_KNOWN_HOSTS_PATH" ]]; then ssh_opts+=(-o "UserKnownHostsFile=$SSH_KNOWN_HOSTS_PATH")fi
remote="${DEPLOY_USER}@${DEPLOY_HOST}"releases_dir="${DEPLOY_PATH}/releases"release_dir="${releases_dir}/${RELEASE_ID}"current_link="${DEPLOY_PATH}/current"
echo "Deploying ${LOCAL_BUILD_DIR} -> ${remote}:${release_dir}"
rsync ${RSYNC_FLAGS} \ -e "ssh ${ssh_opts[*]}" \ "${LOCAL_BUILD_DIR}/" \ "${remote}:${release_dir}/"
ssh "${ssh_opts[@]}" "$remote" bash -s <<EOFset -euo pipefail
ln -sfn "${release_dir}" "${current_link}"
cd "${releases_dir}"to_delete=\$(ls -1dt */ 2>/dev/null | tail -n +$((KEEP_RELEASES + 1)) || true)if [[ -n "\$to_delete" ]]; then echo "Pruning old releases:" echo "\$to_delete" echo "\$to_delete" | while read -r d; do [[ -n "\$d" ]] || continue rm -rf -- "\$d" donefi
EOF
echo "Deployment complete. current -> ${release_dir}"