diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..694936c --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,93 @@ +name: Test and Deploy + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + cache: true + + - name: Download dependencies + run: go mod download + + - name: Run tests + run: go test -v ./... + + deploy: + name: Deploy to Production + needs: test + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + cache: true + + - name: Download dependencies + run: go mod download + + - name: Build Linux binary + run: GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o moon . + + - name: Copy files to server + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + port: ${{ secrets.DEPLOY_PORT || 22 }} + source: "moon,index.html,about.html,calendar.html,static/" + target: "/tmp/moon-deploy" + overwrite: true + + - name: Install and restart service + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + port: ${{ secrets.DEPLOY_PORT || 22 }} + script: | + set -e + + DEPLOY_SRC=/tmp/moon-deploy + + # Install binary + sudo cp "$DEPLOY_SRC/moon" /usr/local/bin/moon + sudo chmod +x /usr/local/bin/moon + + # Update web assets + sudo cp "$DEPLOY_SRC/index.html" /var/www/moon/ + sudo cp "$DEPLOY_SRC/about.html" /var/www/moon/ + sudo cp "$DEPLOY_SRC/calendar.html" /var/www/moon/ + sudo cp -r "$DEPLOY_SRC/static/" /var/www/moon/ + sudo chown -R www-data:www-data /var/www/moon + + # Restart service + sudo systemctl restart moon + + # Verify it came back up + sleep 2 + sudo systemctl is-active moon + + # Clean up + rm -rf "$DEPLOY_SRC" + + echo "Deployment complete" diff --git a/README.md b/README.md index ca91912..c3704ba 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,58 @@ sudo ufw enable sudo ufw status ``` +## CI/CD Deployment (GitHub Actions → Linode) + +Every push to `master` automatically runs tests, builds a Linux binary, and deploys it to your Linode server via SSH. + +### How it works + +1. GitHub Actions runs tests (`go test`) +2. If tests pass, it cross-compiles a Linux amd64 binary +3. It SCPs the binary and web assets to your server +4. It SSHs in and installs them, then restarts the systemd service + +### One-time server setup + +Run the provided setup script **once** on your Linode server: + +```bash +sudo bash scripts/server-setup.sh +``` + +This script: +- Creates a `deploy` user with a generated SSH key pair +- Adds a minimal `sudoers` entry so `deploy` can copy files and restart the service +- Prints the private key and the exact secrets to add to GitHub + +### GitHub repository secrets + +After running the setup script, go to your GitHub repo: +**Settings → Secrets and variables → Actions → New repository secret** + +| Secret name | Value | +|------------------|--------------------------------------------| +| `DEPLOY_HOST` | Your Linode public IP or hostname | +| `DEPLOY_USER` | `deploy` | +| `DEPLOY_SSH_KEY` | The private key printed by the setup script | +| `DEPLOY_PORT` | Your SSH port (only if not port 22) | + +### Triggering a deployment + +Push to `master`: + +```bash +git push origin master +``` + +GitHub Actions will: +1. Run the `Test` job +2. If tests pass, run the `Deploy to Production` job +3. Report success or failure in the Actions tab + +View deployment logs at: +`https://github.com//moon/actions` + ## Testing Run the test suite: @@ -337,20 +389,25 @@ go test -cover ``` moon/ -├── moon.go # Main server application -├── moon_test.go # Unit tests -├── index.html # Home page -├── about.html # About page -├── calendar.html # Calendar view +├── moon.go # Main server application +├── moon_test.go # Unit tests +├── index.html # Home page +├── about.html # About page +├── calendar.html # Calendar view ├── static/ -│ ├── styles.css # Global styles -│ ├── script.js # Client-side JavaScript -│ └── moon.jpg # Background image -├── go.mod # Go module dependencies -├── .env.example # Environment variables template -├── .gitignore # Git ignore rules -├── moon.service # systemd service file -└── README.md # This file +│ ├── styles.css # Global styles +│ ├── script.js # Client-side JavaScript +│ └── moon.jpg # Background image +├── .github/ +│ └── workflows/ +│ └── deploy.yml # GitHub Actions CI/CD pipeline +├── scripts/ +│ └── server-setup.sh # One-time Linode server setup +├── go.mod # Go module dependencies +├── .env.example # Environment variables template +├── .gitignore # Git ignore rules +├── moon.service # systemd service file +└── README.md # This file ``` ## API Endpoints diff --git a/scripts/server-setup.sh b/scripts/server-setup.sh new file mode 100755 index 0000000..7415ccf --- /dev/null +++ b/scripts/server-setup.sh @@ -0,0 +1,109 @@ +#!/bin/bash +# server-setup.sh +# +# One-time setup script to prepare your Linode Debian server for automated +# deployments from GitHub Actions. +# +# Run as root or with sudo: +# sudo bash scripts/server-setup.sh +# +# After running, follow the printed instructions to add the SSH public key +# to your GitHub repository secrets. + +set -e + +DEPLOY_USER="deploy" + +echo "=== Moon App - Server Deployment Setup ===" +echo "" + +# --------------------------------------------------------------- +# 1. Create deploy user +# --------------------------------------------------------------- +if id "$DEPLOY_USER" &>/dev/null; then + echo "[ok] User '$DEPLOY_USER' already exists" +else + useradd -m -s /bin/bash "$DEPLOY_USER" + echo "[ok] Created user '$DEPLOY_USER'" +fi + +# --------------------------------------------------------------- +# 2. Generate SSH key pair for GitHub Actions +# --------------------------------------------------------------- +KEY_DIR="/home/$DEPLOY_USER/.ssh" +KEY_FILE="$KEY_DIR/github_actions" + +mkdir -p "$KEY_DIR" +chmod 700 "$KEY_DIR" + +if [ ! -f "$KEY_FILE" ]; then + ssh-keygen -t ed25519 -f "$KEY_FILE" -N "" -C "github-actions-moon-deploy" + echo "[ok] Generated SSH key pair at $KEY_FILE" +else + echo "[ok] SSH key already exists at $KEY_FILE" +fi + +# Authorise the key for the deploy user +if ! grep -qF "$(cat "$KEY_FILE.pub")" "$KEY_DIR/authorized_keys" 2>/dev/null; then + cat "$KEY_FILE.pub" >> "$KEY_DIR/authorized_keys" + echo "[ok] Public key added to authorized_keys" +fi + +chmod 600 "$KEY_DIR/authorized_keys" +chown -R "$DEPLOY_USER:$DEPLOY_USER" "$KEY_DIR" + +# --------------------------------------------------------------- +# 3. Create sudoers entry (least privilege) +# --------------------------------------------------------------- +SUDOERS_FILE="/etc/sudoers.d/moon-deploy" + +cat > "$SUDOERS_FILE" << 'EOF' +# Allow the deploy user to install the moon app without a password +deploy ALL=(ALL) NOPASSWD: \ + /bin/cp /tmp/moon-deploy/moon /usr/local/bin/moon, \ + /bin/chmod +x /usr/local/bin/moon, \ + /bin/cp /tmp/moon-deploy/index.html /var/www/moon/, \ + /bin/cp /tmp/moon-deploy/about.html /var/www/moon/, \ + /bin/cp /tmp/moon-deploy/calendar.html /var/www/moon/, \ + /bin/cp -r /tmp/moon-deploy/static/ /var/www/moon/, \ + /bin/chown -R www-data\:www-data /var/www/moon, \ + /usr/bin/systemctl restart moon, \ + /usr/bin/systemctl is-active moon +EOF + +chmod 440 "$SUDOERS_FILE" +# Validate the file +visudo -c -f "$SUDOERS_FILE" +echo "[ok] sudoers entry created at $SUDOERS_FILE" + +# --------------------------------------------------------------- +# 4. Ensure /var/www/moon exists and is owned correctly +# --------------------------------------------------------------- +mkdir -p /var/www/moon +chown -R www-data:www-data /var/www/moon +echo "[ok] /var/www/moon ready" + +# --------------------------------------------------------------- +# 5. Print next steps +# --------------------------------------------------------------- +echo "" +echo "=== Setup complete. Add these secrets to your GitHub repository: ===" +echo "" +echo "Go to: GitHub repo → Settings → Secrets and variables → Actions" +echo "" +echo "Secret name : DEPLOY_HOST" +echo "Secret value : $(hostname -I | awk '{print $1}') (your server's public IP)" +echo "" +echo "Secret name : DEPLOY_USER" +echo "Secret value : $DEPLOY_USER" +echo "" +echo "Secret name : DEPLOY_SSH_KEY" +echo "Secret value : (paste the private key below)" +echo "" +echo "---BEGIN PRIVATE KEY (copy everything including the dashes)---" +cat "$KEY_FILE" +echo "---END PRIVATE KEY---" +echo "" +echo "Optional secret : DEPLOY_PORT (only if SSH is not on port 22)" +echo "" +echo "After adding secrets, push to master to trigger your first deployment."