Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -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"
83 changes: 70 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<your-org>/moon/actions`

## Testing

Run the test suite:
Expand All @@ -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
Expand Down
109 changes: 109 additions & 0 deletions scripts/server-setup.sh
Original file line number Diff line number Diff line change
@@ -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."