Autonomous issue-to-PR driver for Claude Code and OpenCode, structured per lair/architecture generic.md. Workspace: entities/core/data/agent library crates plus api, worker and cli binaries. Two pieces of real logic land with tests — lane routing (cc for judgement, oc for specification) and the limit governor. Constraints encoded as code rather than comments: - agents are spawned as vendor binaries; tireless never calls a provider API - ANTHROPIC_API_KEY is never set by tireless, only passed through - assert_not_anthropic refuses to start an OpenCode lane pointed at Anthropic - every run passes the governor; provider rate-limit signals win over our own accounting Deployment assets target bob.hanzalova.internal:23296 (registered in port-allocations.md), fronted by hanzalova at tireless.internal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DHhHtohxcdk1PL3tfnYJdH
145 lines
5.1 KiB
YAML
145 lines
5.1 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"
|
|
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 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
|
|
|
|
- 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
|
|
rsync $R -a --delete dashboard/dist/ \
|
|
gitea_ci@"$API_HOST":"$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 /var/www/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
|
|
sudo systemctl restart tireless-runner.service
|
|
EOF
|
|
|
|
- name: health probe
|
|
run: |
|
|
ssh gitea_ci@"$API_HOST" \
|
|
"curl -fsS http://127.0.0.1:$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"
|