Compare commits
4 Commits
6609956711
...
shorten-sh
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f62a567ee | |||
| efea999a2b | |||
| aed051afaa | |||
| 410eb3a5a8 |
87
.gitea/workflows/deploy.yaml
Normal file
87
.gitea/workflows/deploy.yaml
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
name: Deploy
|
||||||
|
|
||||||
|
# Builds the static frontend and the Go share-service, ships both to the
|
||||||
|
# Caddy host, and restarts the service.
|
||||||
|
#
|
||||||
|
# Required repo secret:
|
||||||
|
# DEPLOY_SSH_KEY - base64-encoded private SSH key authorized for
|
||||||
|
# ${DEPLOY_USER}@${DEPLOY_HOST}. Generate with:
|
||||||
|
# base64 -w0 < deploy_key # Linux
|
||||||
|
# base64 -i deploy_key | tr -d '\n' # macOS
|
||||||
|
#
|
||||||
|
# Override host/user/paths via repo variables (Settings > Actions > Variables)
|
||||||
|
# if they ever change; the defaults below match the Justfile.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
DEPLOY_HOST: ${{ vars.DEPLOY_HOST || 'goldfish.malzahn.lan' }}
|
||||||
|
DEPLOY_USER: ${{ vars.DEPLOY_USER || 'root' }}
|
||||||
|
WWW_ROOT: ${{ vars.WWW_ROOT || '/usr/share/caddy/public_html/fabula' }}
|
||||||
|
GO_ARCH: ${{ vars.GO_ARCH || 'amd64' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# --- Build frontend ---
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build # -> dist/
|
||||||
|
|
||||||
|
# --- Build backend (static, no cgo) ---
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: server/go.mod
|
||||||
|
cache-dependency-path: server/go.sum
|
||||||
|
- name: Build share-svc
|
||||||
|
working-directory: server
|
||||||
|
run: CGO_ENABLED=0 GOOS=linux GOARCH="$GO_ARCH" go build -o share-svc .
|
||||||
|
|
||||||
|
# --- Deploy ---
|
||||||
|
- name: Configure SSH
|
||||||
|
run: |
|
||||||
|
command -v rsync >/dev/null || { apt-get update && apt-get install -y rsync; }
|
||||||
|
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||||
|
# DEPLOY_SSH_KEY is stored base64-encoded so the PEM survives the
|
||||||
|
# secret round-trip intact (no CRLF/whitespace/newline mangling).
|
||||||
|
echo "${{ secrets.DEPLOY_SSH_KEY }}" | base64 -d > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
|
- name: Copy frontend
|
||||||
|
run: |
|
||||||
|
rsync -az --delete -e "ssh -i ~/.ssh/deploy_key" \
|
||||||
|
dist/ "$DEPLOY_USER@$DEPLOY_HOST:$WWW_ROOT/"
|
||||||
|
|
||||||
|
- name: Copy backend binary + unit file
|
||||||
|
run: |
|
||||||
|
# Copy to a temp path then atomically rename into place. You can't
|
||||||
|
# overwrite a running executable in place (ETXTBSY / "Text file busy"),
|
||||||
|
# but rename(2) just swaps the directory entry while the old inode keeps
|
||||||
|
# serving the running process until the restart step picks up the new
|
||||||
|
# binary. chmod first so the exec bit survives the rename (scp doesn't
|
||||||
|
# preserve mode without -p).
|
||||||
|
scp -i ~/.ssh/deploy_key server/share-svc \
|
||||||
|
"$DEPLOY_USER@$DEPLOY_HOST:/usr/local/bin/share-svc.new"
|
||||||
|
ssh -i ~/.ssh/deploy_key "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||||
|
'chmod 0755 /usr/local/bin/share-svc.new && mv -f /usr/local/bin/share-svc.new /usr/local/bin/share-svc'
|
||||||
|
scp -i ~/.ssh/deploy_key server/share-svc.service \
|
||||||
|
"$DEPLOY_USER@$DEPLOY_HOST:/etc/systemd/system/share-svc.service"
|
||||||
|
|
||||||
|
- name: Restart services
|
||||||
|
run: |
|
||||||
|
ssh -i ~/.ssh/deploy_key "$DEPLOY_USER@$DEPLOY_HOST" '
|
||||||
|
systemctl daemon-reload &&
|
||||||
|
systemctl enable --now share-svc &&
|
||||||
|
systemctl restart share-svc &&
|
||||||
|
systemctl reload caddy
|
||||||
|
'
|
||||||
14
Justfile
14
Justfile
@@ -6,8 +6,12 @@ user := "root"
|
|||||||
host := "goldfish.malzahn.lan"
|
host := "goldfish.malzahn.lan"
|
||||||
www-root := "/usr/share/caddy/public_html/fabula"
|
www-root := "/usr/share/caddy/public_html/fabula"
|
||||||
|
|
||||||
|
# Target architecture of the deploy host (override: `just go-arch=arm64 deploy-server`)
|
||||||
|
go-arch := "amd64"
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf dist/*
|
rm -rf dist/*
|
||||||
|
rm -f server/share-svc
|
||||||
|
|
||||||
serve:
|
serve:
|
||||||
npm run dev
|
npm run dev
|
||||||
@@ -18,6 +22,16 @@ build:
|
|||||||
deploy: build
|
deploy: build
|
||||||
scp -r dist/* {{ user }}@{{ host }}:{{ www-root }}/
|
scp -r dist/* {{ user }}@{{ host }}:{{ www-root }}/
|
||||||
|
|
||||||
|
# Build the share service as a static binary for the deploy host.
|
||||||
|
build-server:
|
||||||
|
cd server && CGO_ENABLED=0 GOOS=linux GOARCH={{ go-arch }} go build -o share-svc .
|
||||||
|
|
||||||
|
# Ship the binary + unit file and (re)start the service. Requires root on host.
|
||||||
|
deploy-server: build-server
|
||||||
|
scp server/share-svc {{ user }}@{{ host }}:/usr/local/bin/share-svc
|
||||||
|
scp server/share-svc.service {{ user }}@{{ host }}:/etc/systemd/system/share-svc.service
|
||||||
|
ssh {{ user }}@{{ host }} 'systemctl daemon-reload && systemctl enable --now share-svc && systemctl restart share-svc'
|
||||||
|
|
||||||
format:
|
format:
|
||||||
npx prettier --write books/
|
npx prettier --write books/
|
||||||
npx prettier --write src/
|
npx prettier --write src/
|
||||||
|
|||||||
21
server/Caddyfile.snippet
Normal file
21
server/Caddyfile.snippet
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Caddy config for the share-link service.
|
||||||
|
#
|
||||||
|
# The frontend (served under /fabula/) calls /fabula/api/s, but the Go service
|
||||||
|
# routes plain /api/s. `uri strip_prefix /fabula` rewrites /fabula/api/s ->
|
||||||
|
# /api/s before proxying to the service on loopback.
|
||||||
|
#
|
||||||
|
# Place this INSIDE your existing site block, and make sure this /fabula/api/*
|
||||||
|
# handler comes BEFORE the static handler that serves /fabula/* — `handle`
|
||||||
|
# blocks match in source order, first match wins.
|
||||||
|
|
||||||
|
handle /fabula/api/* {
|
||||||
|
uri strip_prefix /fabula
|
||||||
|
reverse_proxy 127.0.0.1:8090
|
||||||
|
}
|
||||||
|
|
||||||
|
# ... your existing static file handler for /fabula/* follows here, e.g.:
|
||||||
|
#
|
||||||
|
# handle /fabula/* {
|
||||||
|
# root * /usr/share/caddy/public_html
|
||||||
|
# file_server
|
||||||
|
# }
|
||||||
51
server/README.md
Normal file
51
server/README.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# share-svc
|
||||||
|
|
||||||
|
Tiny Go + SQLite service that stores character-sheet JSON blobs and returns
|
||||||
|
short, content-addressed IDs, so sheets share as `...?s=<id>` instead of a giant
|
||||||
|
inline `?c=` payload.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
| Method | Path | Body | Response |
|
||||||
|
| ------ | ------------ | ----------- | ------------------- |
|
||||||
|
| POST | `/api/s` | sheet JSON | `{"id":"<id>"}` |
|
||||||
|
| GET | `/api/s/{id}`| – | sheet JSON |
|
||||||
|
|
||||||
|
IDs are `base64url(sha256(json))[:12]`, so identical sheets dedupe and re-sharing
|
||||||
|
is idempotent. Payloads are stored deflated in a single SQLite file.
|
||||||
|
|
||||||
|
### Config (env)
|
||||||
|
|
||||||
|
| Var | Default | Notes |
|
||||||
|
| ------------- | ------------- | ------------------------------------------------ |
|
||||||
|
| `ADDR` | `:8090` | Listen address. The unit binds `127.0.0.1:8090`. |
|
||||||
|
| `DB_PATH` | `shares.db` | SQLite file path. |
|
||||||
|
| `CORS_ORIGIN` | _(unset)_ | Set to allow a cross-origin dev frontend. |
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
From the repo root:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
just deploy-server # build (linux/amd64) + ship + restart
|
||||||
|
just go-arch=arm64 deploy-server # if the host is arm64
|
||||||
|
```
|
||||||
|
|
||||||
|
That installs `/usr/local/bin/share-svc` and `share-svc.service`, then
|
||||||
|
`daemon-reload` + `enable --now` + `restart`. The SQLite DB lives in
|
||||||
|
`/var/lib/share-svc/` (created by the unit's `StateDirectory`).
|
||||||
|
|
||||||
|
### One-time Caddy setup
|
||||||
|
|
||||||
|
Add the `/fabula/api/*` reverse-proxy block from `Caddyfile.snippet` to your site
|
||||||
|
block (before the static `/fabula/*` handler) and reload Caddy.
|
||||||
|
|
||||||
|
## Local dev
|
||||||
|
|
||||||
|
`webpack serve` (port 8080) does not serve `/api/*`, so `?s=` sharing falls back
|
||||||
|
to inline `?c=` links. To exercise short links locally, run the service and point
|
||||||
|
the frontend at it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
CORS_ORIGIN=http://localhost:8080 go run ./server
|
||||||
|
```
|
||||||
35
server/share-svc.service
Normal file
35
server/share-svc.service
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Fabula Ultima share-link service
|
||||||
|
Documentation=https://git.illaoi.pro/drew/fabula-ultima-html
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/local/bin/share-svc
|
||||||
|
# Bind to loopback only; Caddy reverse-proxies public traffic to it.
|
||||||
|
Environment=ADDR=127.0.0.1:8090
|
||||||
|
Environment=DB_PATH=/var/lib/share-svc/shares.db
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=2
|
||||||
|
|
||||||
|
# Run as an ephemeral, unprivileged user. StateDirectory creates and chowns
|
||||||
|
# /var/lib/share-svc so the SQLite file persists across restarts.
|
||||||
|
DynamicUser=yes
|
||||||
|
StateDirectory=share-svc
|
||||||
|
|
||||||
|
# Hardening
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
PrivateDevices=yes
|
||||||
|
ProtectKernelTunables=yes
|
||||||
|
ProtectKernelModules=yes
|
||||||
|
ProtectControlGroups=yes
|
||||||
|
RestrictAddressFamilies=AF_INET AF_INET6
|
||||||
|
RestrictNamespaces=yes
|
||||||
|
LockPersonality=yes
|
||||||
|
MemoryDenyWriteExecute=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user