Three artefacts disagreed about where nginx runs. design.md §6.2 and the vhost both said the hanzalova proxy; the API bound 127.0.0.1 and the workflow rsynced the dashboard to bob. That combination deploys green and then serves nothing, since a proxy on another host cannot reach bob's loopback. Resolve it the way design.md already stated: nginx on the proxy, dashboard shipped there, API bound 0.0.0.0 behind firewalld and the mesh. The health probe now runs from the proxy over the mesh rather than from bob's loopback, so it fails when firewalld is closed instead of passing regardless. infra-setup.sh grows a proxy grant scoped to static files alone, and the nginx vhost install as a manual step — it needs a certificate, and nothing was telling the operator to install it at all. npm run lint had never run: eslint 9 needs a flat config and there was none. Add it, ignoring the ts-rs generated bindings, and run it in CI so it stays true. Untrack dashboard/tsconfig.tsbuildinfo, a build artifact that would have put a spurious diff in every pull request tireless opens. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TxK1CWPkFXqdcXMJ4hVe6
166 lines
6.2 KiB
YAML
166 lines
6.2 KiB
YAML
name: deploy
|
|
|
|
# The workflow is the source of infra truth: hosts, ports and paths live here,
|
|
# not in a separate manifest (architecture/deployment-gitea-actions.md).
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: deploy
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
API_HOST: bob.hanzalova.internal
|
|
API_PORT: "23296"
|
|
# Ingress is the office proxy, not bob (doc/plan/design.md §6.2): nginx there
|
|
# serves the static dashboard and reverse-proxies /v1 across the mesh. The
|
|
# dashboard therefore ships to the proxy, and only the binaries ship to bob.
|
|
WEB_HOST: hanzalova.internal
|
|
WEB_ROOT: /var/www/tireless
|
|
VITE_API_BASE_URL: ""
|
|
|
|
jobs:
|
|
build:
|
|
runs-on: fedora-43-rust
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
# Quality gate first: a commit that fails lint or tests never deploys.
|
|
- name: format
|
|
run: cargo fmt --all --check
|
|
- name: lint
|
|
run: cargo clippy --all-targets --all-features -- -D warnings
|
|
- name: test
|
|
run: cargo test --workspace
|
|
|
|
# Static build so a runner newer than the target cannot produce a binary
|
|
# the target's glibc rejects (§6 glibc skew).
|
|
- name: build binaries
|
|
run: cargo build --release --target x86_64-unknown-linux-musl
|
|
|
|
- name: build dashboard
|
|
working-directory: dashboard
|
|
run: |
|
|
npm ci
|
|
npm run lint
|
|
npm run build
|
|
|
|
- uses: actions/upload-artifact@v3
|
|
with:
|
|
name: tireless
|
|
path: |
|
|
target/x86_64-unknown-linux-musl/release/tireless-api
|
|
target/x86_64-unknown-linux-musl/release/tireless-worker
|
|
target/x86_64-unknown-linux-musl/release/tireless
|
|
dashboard/dist/
|
|
asset/
|
|
|
|
deploy:
|
|
needs: build
|
|
runs-on: fedora-43
|
|
steps:
|
|
- uses: actions/download-artifact@v3
|
|
with:
|
|
name: tireless
|
|
|
|
- name: authorise
|
|
run: |
|
|
install -d -m 0700 ~/.ssh
|
|
printf '%s\n' "${{ secrets.RSYNC_SSH_KEY }}" > ~/.ssh/id_gitea_ci
|
|
chmod 0600 ~/.ssh/id_gitea_ci
|
|
cat >> ~/.ssh/config <<EOF
|
|
Host *
|
|
IdentityFile ~/.ssh/id_gitea_ci
|
|
StrictHostKeyChecking accept-new
|
|
EOF
|
|
ssh gitea_ci@"$API_HOST" hostname -f
|
|
ssh gitea_ci@"$WEB_HOST" hostname -f
|
|
|
|
- name: render config
|
|
env:
|
|
DEPLOY_HOST_FQDN: ${{ env.API_HOST }}
|
|
run: |
|
|
# Literal substitution so secrets containing shell metacharacters survive.
|
|
python3 - <<'PY'
|
|
import os, pathlib
|
|
tmpl = pathlib.Path("asset/config/config.toml.tmpl").read_text()
|
|
for key in ("DEPLOY_HOST_FQDN",):
|
|
tmpl = tmpl.replace("{{%s}}" % key, os.environ[key])
|
|
pathlib.Path("config.toml").write_text(tmpl)
|
|
PY
|
|
|
|
- name: ship artifacts
|
|
run: |
|
|
R="--rsync-path=sudo rsync --mkpath"
|
|
rsync $R --chmod 0755 target/x86_64-unknown-linux-musl/release/tireless-api \
|
|
gitea_ci@"$API_HOST":/usr/local/bin/tireless-api
|
|
rsync $R --chmod 0755 target/x86_64-unknown-linux-musl/release/tireless-worker \
|
|
gitea_ci@"$API_HOST":/usr/local/bin/tireless-worker
|
|
rsync $R --chmod 0755 target/x86_64-unknown-linux-musl/release/tireless \
|
|
gitea_ci@"$API_HOST":/usr/local/bin/tireless
|
|
rsync $R --chmod 0640 config.toml \
|
|
gitea_ci@"$API_HOST":/etc/tireless/config.toml
|
|
rsync $R asset/systemd/tireless.sysusers.conf \
|
|
gitea_ci@"$API_HOST":/etc/sysusers.d/tireless.conf
|
|
for unit in tireless-api tireless-poller tireless-runner; do
|
|
rsync $R "asset/systemd/$unit.service" \
|
|
gitea_ci@"$API_HOST":"/etc/systemd/system/$unit.service"
|
|
done
|
|
rsync $R asset/firewalld/tireless-api.xml \
|
|
gitea_ci@"$API_HOST":/etc/firewalld/services/tireless-api.xml
|
|
|
|
- name: ship dashboard
|
|
run: |
|
|
# To the proxy, not to bob — that is where nginx serves it from.
|
|
R="--rsync-path=sudo rsync --mkpath"
|
|
rsync $R -a --delete dashboard/dist/ \
|
|
gitea_ci@"$WEB_HOST":"$WEB_ROOT/"
|
|
ssh gitea_ci@"$WEB_HOST" "sudo restorecon -R $WEB_ROOT"
|
|
|
|
- name: apply system state
|
|
run: |
|
|
ssh gitea_ci@"$API_HOST" bash -euo pipefail <<EOF
|
|
sudo systemd-sysusers
|
|
sudo restorecon -R /usr/local/bin/tireless-api /usr/local/bin/tireless-worker \
|
|
/usr/local/bin/tireless /etc/tireless /var/lib/tireless
|
|
|
|
# firewalld only learns a freshly-shipped service after a reload (§6).
|
|
sudo firewall-cmd --reload
|
|
zone=\$(sudo firewall-cmd --get-default-zone)
|
|
sudo firewall-cmd --zone=\$zone --query-service=tireless-api \
|
|
|| { sudo firewall-cmd --permanent --zone=\$zone --add-service=tireless-api; \
|
|
sudo firewall-cmd --zone=\$zone --add-service=tireless-api; }
|
|
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl restart tireless-api.service
|
|
sudo systemctl restart tireless-poller.service
|
|
|
|
# The runner is restarted last and deliberately: it may be mid-agent-run,
|
|
# and TimeoutStopSec=120 will not wait out a job with an hour-long
|
|
# ceiling. A killed run is not lost — its claim lease expires and the
|
|
# job returns to the pool (design.md §4.2) — but it does cost the tokens
|
|
# already spent. That is the accepted trade for a simple deploy; see
|
|
# design.md §10 for why it bites hardest when tireless deploys itself.
|
|
sudo systemctl restart tireless-runner.service
|
|
EOF
|
|
|
|
- name: health probe
|
|
run: |
|
|
# Probe from the proxy, over the mesh — the path a user actually takes.
|
|
# A loopback probe on bob would pass even if firewalld were closed.
|
|
ssh gitea_ci@"$WEB_HOST" \
|
|
"curl -fsS http://$API_HOST:$API_PORT/v1/ready"
|
|
for unit in tireless-api tireless-poller tireless-runner; do
|
|
ssh gitea_ci@"$API_HOST" "systemctl is-active \$unit.service"
|
|
done
|
|
|
|
- name: startup journal
|
|
if: always()
|
|
run: |
|
|
ssh gitea_ci@"$API_HOST" \
|
|
"journalctl -u tireless-api -u tireless-poller -u tireless-runner \
|
|
--since '5 minutes ago' --no-pager"
|