Skip to content

Commit ddef12c

Browse files
committed
ci: add automated server deployment
1 parent c4b0356 commit ddef12c

3 files changed

Lines changed: 516 additions & 0 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
name: Deploy Server
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
permissions:
9+
contents: read
10+
11+
concurrency:
12+
group: deploy-server-${{ github.ref }}
13+
cancel-in-progress: false
14+
15+
defaults:
16+
run:
17+
shell: bash
18+
19+
jobs:
20+
deploy:
21+
runs-on: ubuntu-latest
22+
timeout-minutes: 20
23+
24+
steps:
25+
- name: Check out repository
26+
uses: actions/checkout@v4
27+
28+
- name: Set up Go
29+
uses: actions/setup-go@v5
30+
with:
31+
go-version-file: clipSync-server/go.mod
32+
33+
- name: Run server tests
34+
working-directory: clipSync-server
35+
run: go test ./... -v -count=1
36+
37+
- name: Build Linux release binary
38+
working-directory: clipSync-server
39+
env:
40+
GOOS: linux
41+
GOARCH: amd64
42+
CGO_ENABLED: "1"
43+
run: go build -o bin/clipsync-server-linux ./cmd/server
44+
45+
- name: Assemble release bundle
46+
env:
47+
RELEASE_ARCHIVE: clipsync-server-release-${{ github.sha }}.tar.gz
48+
run: |
49+
mkdir -p release/clipSync-server/bin release/clipSync-server/configs
50+
install -m 0755 clipSync-server/bin/clipsync-server-linux release/clipSync-server/bin/clipsync-server-linux
51+
install -m 0644 clipSync-server/configs/config.yaml release/clipSync-server/configs/config.yaml
52+
tar -C release -czf "$RELEASE_ARCHIVE" clipSync-server
53+
echo "RELEASE_ARCHIVE=$RELEASE_ARCHIVE" >> "$GITHUB_ENV"
54+
55+
- name: Prepare SSH
56+
env:
57+
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
58+
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
59+
run: |
60+
install -d -m 700 ~/.ssh
61+
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
62+
chmod 600 ~/.ssh/id_ed25519
63+
printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
64+
chmod 644 ~/.ssh/known_hosts
65+
66+
- name: Upload release archive
67+
env:
68+
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
69+
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
70+
run: |
71+
ssh_opts=(
72+
-o BatchMode=yes
73+
-o StrictHostKeyChecking=yes
74+
-o ConnectTimeout=10
75+
-o ConnectionAttempts=3
76+
-o ServerAliveInterval=15
77+
-o ServerAliveCountMax=3
78+
)
79+
remote_archive="/tmp/${RELEASE_ARCHIVE}"
80+
scp "${ssh_opts[@]}" "$RELEASE_ARCHIVE" "${DEPLOY_USER}@${DEPLOY_HOST}:${remote_archive}"
81+
echo "REMOTE_ARCHIVE=$remote_archive" >> "$GITHUB_ENV"
82+
83+
- name: Run remote deployment
84+
env:
85+
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
86+
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
87+
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
88+
DEPLOY_SERVICE_NAME: ${{ secrets.DEPLOY_SERVICE_NAME }}
89+
run: |
90+
ssh_opts=(
91+
-o BatchMode=yes
92+
-o StrictHostKeyChecking=yes
93+
-o ConnectTimeout=10
94+
-o ConnectionAttempts=3
95+
-o ServerAliveInterval=15
96+
-o ServerAliveCountMax=3
97+
)
98+
{
99+
printf 'export DEPLOY_ARCHIVE=%q\n' "$REMOTE_ARCHIVE"
100+
printf 'export DEPLOY_PATH=%q\n' "$DEPLOY_PATH"
101+
printf 'export DEPLOY_SERVICE_NAME=%q\n' "$DEPLOY_SERVICE_NAME"
102+
cat scripts/deploy/server-release.sh
103+
} | ssh "${ssh_opts[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "bash -s"
104+
105+
- name: Verify deployed health endpoint
106+
env:
107+
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
108+
DEPLOY_PUBLIC_HEALTH_URL: ${{ secrets.DEPLOY_PUBLIC_HEALTH_URL }}
109+
run: |
110+
health_url="${DEPLOY_PUBLIC_HEALTH_URL:-http://${DEPLOY_HOST}:8081/api/v1/health}"
111+
112+
for attempt in {1..10}; do
113+
if response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 15 "$health_url")"; then
114+
if grep -Eq '"status"[[:space:]]*:[[:space:]]*"ok"' <<<"$response"; then
115+
echo "Health check passed on attempt $attempt"
116+
exit 0
117+
fi
118+
fi
119+
120+
sleep 5
121+
done
122+
123+
echo "Health check failed for $health_url" >&2
124+
exit 1
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# GitHub Actions Server Deployment
2+
3+
## What This Automates
4+
5+
Pushing to `main` triggers [.github/workflows/deploy-server.yml](/C:/Users/20562/Desktop/桌面/clipSync/.github/workflows/deploy-server.yml). The workflow:
6+
7+
- Checks out the repo on `ubuntu-latest`
8+
- Sets up Go from `clipSync-server/go.mod`
9+
- Runs `go test ./... -v -count=1` in `clipSync-server`
10+
- Builds a Linux `amd64` server binary with `CGO_ENABLED=1`
11+
- Packages the binary and `clipSync-server/configs/config.yaml` into `clipsync-server-release-<git-sha>.tar.gz`
12+
- Uploads the release archive to the deployment host over SSH/SCP
13+
- Pipes [`scripts/deploy/server-release.sh`](/C:/Users/20562/Desktop/桌面/clipSync/scripts/deploy/server-release.sh) to the remote host and executes it with deployment environment variables
14+
- Verifies `http://<DEPLOY_HOST>:8081/api/v1/health` from GitHub Actions after the remote deployment finishes
15+
16+
The current production examples are host `8.141.100.238` and live path `/opt/clipSync-server-src`, but the GitHub secrets are the source of truth.
17+
18+
## Required GitHub Secrets
19+
20+
Configure these repository secrets before enabling the workflow:
21+
22+
| Secret | Example | Purpose |
23+
| --- | --- | --- |
24+
| `DEPLOY_HOST` | `8.141.100.238` | SSH target used by `scp`, `ssh`, and the final external health check |
25+
| `DEPLOY_USER` | `root` | SSH user on the deployment host |
26+
| `DEPLOY_SSH_KEY` | private key contents for the deploy user | Written to `~/.ssh/id_ed25519` inside the workflow |
27+
| `DEPLOY_PATH` | `/opt/clipSync-server-src` | Live server directory that receives `bin/`, `configs/`, and preserved `data/` |
28+
| `DEPLOY_SERVICE_NAME` | `clipsync.service` | systemd service restarted during deploy and rollback |
29+
| `DEPLOY_KNOWN_HOSTS` | output of `ssh-keyscan -H 8.141.100.238` | Host key entry used for strict SSH host verification |
30+
31+
Notes:
32+
33+
- `DEPLOY_SSH_KEY` must match the public key installed for `DEPLOY_USER` on the server.
34+
- `DEPLOY_KNOWN_HOSTS` is required because the workflow uses `StrictHostKeyChecking=yes`.
35+
- Keep `DEPLOY_PATH` and `DEPLOY_SERVICE_NAME` aligned with the actual server layout and systemd unit.
36+
- Optional: `DEPLOY_PUBLIC_HEALTH_URL` can override the final GitHub Actions health-check URL when the public endpoint differs from `http://<DEPLOY_HOST>:8081/api/v1/health`.
37+
38+
## Server Requirements
39+
40+
The workflow assumes the target server already has:
41+
42+
- A Linux environment reachable from GitHub-hosted runners over SSH
43+
- A systemd service matching `DEPLOY_SERVICE_NAME`
44+
- `bash`, `tar`, `curl`, and `systemctl` available on the target host
45+
- A writable deployment directory such as `/opt/clipSync-server-src`
46+
- Permission for `DEPLOY_USER` to write under `DEPLOY_PATH`
47+
- Permission for `DEPLOY_USER` to restart `DEPLOY_SERVICE_NAME`
48+
- The service configured to run the deployed binary from `DEPLOY_PATH/bin/clipsync-server-linux`
49+
- The service configured so the server can find `configs/config.yaml` after startup
50+
- The server health endpoint available at `http://127.0.0.1:8081/api/v1/health`
51+
52+
Because the workflow builds on GitHub Actions, the target server does not need a Go toolchain for deployments.
53+
54+
The server process defaults to the relative path `configs/config.yaml`. That means the systemd unit must do one of the following:
55+
56+
- Set `WorkingDirectory=DEPLOY_PATH` so the default relative config path resolves correctly.
57+
- Export `CLIPSYNC_CONFIG=DEPLOY_PATH/configs/config.yaml` so the binary can start from any working directory.
58+
59+
`ssh` and `scp` are only required on the GitHub Actions runner, not on the deployment host.
60+
61+
## First-Time Setup
62+
63+
1. Confirm the repository version of `clipSync-server/configs/config.yaml` is safe for the deployment environment.
64+
2. Create the required GitHub repository secrets listed above.
65+
3. Install the deploy public key for `DEPLOY_USER` on the server.
66+
4. Capture the server host key for `DEPLOY_KNOWN_HOSTS`.
67+
68+
```bash
69+
ssh-keyscan -H 8.141.100.238
70+
```
71+
72+
5. Ensure the live directory exists and matches the expected layout.
73+
74+
```bash
75+
sudo mkdir -p /opt/clipSync-server-src/bin /opt/clipSync-server-src/configs /opt/clipSync-server-src/data
76+
```
77+
78+
6. Ensure the systemd service points at the deployed binary and satisfies the config lookup requirement.
79+
80+
Example expectations:
81+
82+
- Binary: `/opt/clipSync-server-src/bin/clipsync-server-linux`
83+
- Config: `/opt/clipSync-server-src/configs/config.yaml`
84+
- Data directory: `/opt/clipSync-server-src/data`
85+
- Either `WorkingDirectory=/opt/clipSync-server-src` or `Environment=CLIPSYNC_CONFIG=/opt/clipSync-server-src/configs/config.yaml`
86+
87+
7. Verify the service can start and answer its local health endpoint before relying on automation.
88+
89+
```bash
90+
curl --fail http://127.0.0.1:8081/api/v1/health
91+
```
92+
93+
8. Merge or push a safe change to `main`, then watch the `Deploy Server` workflow in GitHub Actions.
94+
95+
9. If `8081` is not directly reachable from GitHub-hosted runners, add `DEPLOY_PUBLIC_HEALTH_URL` and point it at the real public health endpoint exposed by your proxy or load balancer.
96+
97+
## Deployment Behavior
98+
99+
The remote deployment script is intentionally narrow and opinionated:
100+
101+
- `data/` is preserved. The script creates `DEPLOY_PATH/data` if needed and never deletes or replaces it.
102+
- `configs/config.yaml` is overwritten from the repository on every deploy.
103+
- The live binary path is `DEPLOY_PATH/bin/clipsync-server-linux`.
104+
- The binary backup path is `DEPLOY_PATH/bin/clipsync-server-linux.prev`.
105+
- The config backup path is `DEPLOY_PATH/configs/config.yaml.prev`.
106+
- The uploaded release archive is stored remotely at `/tmp/clipsync-server-release-<git-sha>.tar.gz`.
107+
- The script extracts into a temporary staging directory under `/tmp/clipsync-release.XXXXXX`.
108+
- Archive contents are validated before extraction to reject absolute paths, path traversal, and unsupported entry types.
109+
110+
Rollback behavior:
111+
112+
- Before mutating the live deployment, the script backs up the current binary and config if they exist.
113+
- If an unexpected shell error happens after mutation starts, the script attempts to restore the backups through an `ERR` trap.
114+
- If `systemctl restart <DEPLOY_SERVICE_NAME>` fails, the script restores backups, restarts the service again, and waits for health recovery.
115+
- If the post-deploy local health check fails, the same rollback flow runs.
116+
- Rollback only restores files that actually had prior live versions. On a first deploy, there may be nothing to restore.
117+
- Rollback covers the deployed binary and `config.yaml` only. It does not roll back SQLite database state under `data/`.
118+
- Because the server runs embedded migrations on startup, schema changes must remain backward-compatible with the previous binary if you want rollback to be safe.
119+
- The remote archive is deleted only after a successful deployment. If deployment fails, the archive usually remains in `/tmp/` for inspection.
120+
121+
Health validation happens twice:
122+
123+
- On the server: `server-release.sh` checks `http://127.0.0.1:8081/api/v1/health` up to 10 times with 3-second sleeps.
124+
- In GitHub Actions: the workflow checks `DEPLOY_PUBLIC_HEALTH_URL` when that optional secret is set; otherwise it falls back to `http://<DEPLOY_HOST>:8081/api/v1/health`, up to 10 times with 5-second sleeps.
125+
126+
## Troubleshooting
127+
128+
### SSH Failures
129+
130+
If the workflow fails during `Prepare SSH`, `Upload release archive`, or `Run remote deployment`:
131+
132+
- Re-check `DEPLOY_HOST`, `DEPLOY_USER`, and `DEPLOY_SSH_KEY`.
133+
- Rebuild `DEPLOY_KNOWN_HOSTS` from the current server host key.
134+
135+
```bash
136+
ssh-keyscan -H 8.141.100.238
137+
```
138+
139+
- Confirm the private key in `DEPLOY_SSH_KEY` matches an authorized public key for `DEPLOY_USER`.
140+
- Confirm the server allows SSH from GitHub-hosted runners and is reachable on port `22`.
141+
- Test from a trusted machine with strict host checking enabled:
142+
143+
```bash
144+
ssh -o BatchMode=yes -o StrictHostKeyChecking=yes root@8.141.100.238
145+
```
146+
147+
- If authentication works locally but fails in Actions, re-check line breaks and full key contents in the GitHub secret.
148+
149+
### Health-Check Failures
150+
151+
There are two different health checks, so first identify which one failed:
152+
153+
- Remote script failure means the service did not become healthy on `127.0.0.1:8081`.
154+
- Final workflow failure means the service may be healthy locally, but `http://<DEPLOY_HOST>:8081/api/v1/health` was not reachable or did not return `"status":"ok"` externally.
155+
- If `DEPLOY_PUBLIC_HEALTH_URL` is configured, final workflow failure instead refers to that public URL.
156+
157+
Useful checks on the server:
158+
159+
```bash
160+
systemctl status <DEPLOY_SERVICE_NAME> --no-pager
161+
journalctl -u <DEPLOY_SERVICE_NAME> -n 100 --no-pager
162+
curl --fail http://127.0.0.1:8081/api/v1/health
163+
ls -l /opt/clipSync-server-src/bin/clipsync-server-linux*
164+
ls -l /opt/clipSync-server-src/configs/config.yaml*
165+
```
166+
167+
What to verify:
168+
169+
- The service name in `DEPLOY_SERVICE_NAME` is correct.
170+
- The deployed config in `/opt/clipSync-server-src/configs/config.yaml` contains production-safe values.
171+
- The service is actually starting the binary at `/opt/clipSync-server-src/bin/clipsync-server-linux`.
172+
- Port `8081` is listening and reachable from outside the host if the final GitHub Actions health check is using the default URL.
173+
- If `DEPLOY_PUBLIC_HEALTH_URL` is configured, verify that public endpoint and any proxy/load-balancer routing in front of it.
174+
- If rollback ran, check whether `.prev` files were restored and whether the service recovered to the previous version.
175+
176+
## Self-Review Checklist
177+
178+
Before treating this flow as ready:
179+
180+
- Confirm the secrets in GitHub match the real server.
181+
- Confirm `clipSync-server/configs/config.yaml` is intended to overwrite production on every push to `main`.
182+
- Confirm `DEPLOY_PUBLIC_HEALTH_URL` is set if the API port is not publicly reachable from GitHub-hosted runners.
183+
- Confirm the systemd service still uses the same binary path and health endpoint.
184+
- Confirm operators understand that `data/` is preserved but config is not, and that rollback does not restore SQLite database state.

0 commit comments

Comments
 (0)