fix(deploy): quote --rsync-path, and let systemd own the state directory
Some checks failed
deploy / deploy (push) Failing after 5m49s
Some checks failed
deploy / deploy (push) Failing after 5m49s
Run 4 failed at `ship artifacts` with `sudo: unrecognized option '--server'`. `R="--rsync-path=sudo rsync --mkpath"` expanded unquoted as `rsync $R` splits into three arguments — `--rsync-path=sudo`, plus a stray `rsync` that rsync reads as a source path — so the remote end ran `sudo --server`. Use an array. The dashboard step quoted it inline and was unaffected, which is why only half the deploy was broken. Every rsync destination and every sudo command in `apply system state` has now been exercised directly against bob as gitea_ci, rather than by another six minute round trip: seven rsync targets, sysusers, restorecon, firewalld and daemon-reload all pass. That surfaced the second fault. restorecon was given /var/lib/tireless, which does not exist on a fresh host: infra-setup.sh tried to create it before systemd-sysusers had created the account to own it, so the attempt always raced and always lost. Declare StateDirectory=tireless on all three units instead — systemd creates the directory, owns it as the service user and labels it — and drop the path from restorecon, the grant, and infra-setup. Refs #9 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TxK1CWPkFXqdcXMJ4hVe6
This commit is contained in:
@@ -96,23 +96,32 @@ jobs:
|
||||
|
||||
- name: ship artifacts
|
||||
run: |
|
||||
R="--rsync-path=sudo rsync --mkpath"
|
||||
# An array, not a string. `R="--rsync-path=sudo rsync --mkpath"` used
|
||||
# as `rsync $R` word-splits into three arguments — `--rsync-path=sudo`
|
||||
# plus a stray `rsync` that rsync reads as a source path — and the
|
||||
# remote end runs `sudo --server`, which sudo rejects.
|
||||
#
|
||||
# --mkpath because rsync will not create a missing destination
|
||||
# directory for a single-file copy, and Fedora ships neither
|
||||
# /etc/sysusers.d nor /etc/firewalld/services
|
||||
# (architecture/deployment-gitea-actions.md §6).
|
||||
R=(--rsync-path="sudo rsync --mkpath")
|
||||
B=target/x86_64-unknown-linux-musl/release
|
||||
rsync $R --chmod 0755 "$B/tireless-api" \
|
||||
rsync "${R[@]}" --chmod 0755 "$B/tireless-api" \
|
||||
gitea_ci@"$API_HOST":/usr/local/bin/tireless-api
|
||||
rsync $R --chmod 0755 "$B/tireless-worker" \
|
||||
rsync "${R[@]}" --chmod 0755 "$B/tireless-worker" \
|
||||
gitea_ci@"$API_HOST":/usr/local/bin/tireless-worker
|
||||
rsync $R --chmod 0755 "$B/tireless" \
|
||||
rsync "${R[@]}" --chmod 0755 "$B/tireless" \
|
||||
gitea_ci@"$API_HOST":/usr/local/bin/tireless
|
||||
rsync $R --chmod 0640 config.toml \
|
||||
rsync "${R[@]}" --chmod 0640 config.toml \
|
||||
gitea_ci@"$API_HOST":/etc/tireless/config.toml
|
||||
rsync $R asset/systemd/tireless.sysusers.conf \
|
||||
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" \
|
||||
rsync "${R[@]}" "asset/systemd/$unit.service" \
|
||||
gitea_ci@"$API_HOST":"/etc/systemd/system/$unit.service"
|
||||
done
|
||||
rsync $R asset/firewalld/tireless-api.xml \
|
||||
rsync "${R[@]}" asset/firewalld/tireless-api.xml \
|
||||
gitea_ci@"$API_HOST":/etc/firewalld/services/tireless-api.xml
|
||||
|
||||
- name: ship dashboard
|
||||
@@ -129,8 +138,11 @@ jobs:
|
||||
run: |
|
||||
ssh gitea_ci@"$API_HOST" bash -euo pipefail <<EOF
|
||||
sudo systemd-sysusers
|
||||
# /var/lib/tireless is absent here on purpose: it does not exist yet on
|
||||
# a fresh host, and the units declare StateDirectory=tireless, so
|
||||
# systemd creates, owns and labels it at first start.
|
||||
sudo restorecon -R /usr/local/bin/tireless-api /usr/local/bin/tireless-worker \
|
||||
/usr/local/bin/tireless /etc/tireless /var/lib/tireless
|
||||
/usr/local/bin/tireless /etc/tireless
|
||||
|
||||
# firewalld only learns a freshly-shipped service after a reload
|
||||
# (architecture/deployment-gitea-actions.md §6).
|
||||
|
||||
@@ -29,7 +29,12 @@ LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
SystemCallArchitectures=native
|
||||
|
||||
ReadWritePaths=/var/lib/tireless
|
||||
# StateDirectory rather than a bare ReadWritePaths: systemd creates
|
||||
# /var/lib/tireless on first start, owns it as the service user, and labels it
|
||||
# for SELinux. A plain ReadWritePaths requires the directory to already exist,
|
||||
# which on a fresh host it does not — the service account is created by
|
||||
# systemd-sysusers during the same deploy that first starts these units.
|
||||
StateDirectory=tireless
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -30,7 +30,12 @@ LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
SystemCallArchitectures=native
|
||||
|
||||
ReadWritePaths=/var/lib/tireless
|
||||
# StateDirectory rather than a bare ReadWritePaths: systemd creates
|
||||
# /var/lib/tireless on first start, owns it as the service user, and labels it
|
||||
# for SELinux. A plain ReadWritePaths requires the directory to already exist,
|
||||
# which on a fresh host it does not — the service account is created by
|
||||
# systemd-sysusers during the same deploy that first starts these units.
|
||||
StateDirectory=tireless
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -58,7 +58,12 @@ MemoryDenyWriteExecute=false
|
||||
# The runner clones repos, runs builds and spawns agents, all under its state
|
||||
# directory. PrivateTmp gives it an isolated /tmp for the toolchains that insist
|
||||
# on one.
|
||||
ReadWritePaths=/var/lib/tireless
|
||||
# StateDirectory rather than a bare ReadWritePaths: systemd creates
|
||||
# /var/lib/tireless on first start, owns it as the service user, and labels it
|
||||
# for SELinux. A plain ReadWritePaths requires the directory to already exist,
|
||||
# which on a fresh host it does not — the service account is created by
|
||||
# systemd-sysusers during the same deploy that first starts these units.
|
||||
StateDirectory=tireless
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -70,7 +70,7 @@ gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl daemon-reload
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl restart tireless-api.service
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl restart tireless-poller.service
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl restart tireless-runner.service
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/restorecon -R /usr/local/bin/tireless-api /usr/local/bin/tireless-worker /usr/local/bin/tireless /etc/tireless /var/lib/tireless
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/restorecon -R /usr/local/bin/tireless-api /usr/local/bin/tireless-worker /usr/local/bin/tireless /etc/tireless
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/semanage port -l
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/semanage port -a -t http_port_t -p tcp ${API_PORT}
|
||||
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --reload
|
||||
@@ -85,8 +85,10 @@ SUDOERS
|
||||
# 2. Service account, directories, cert ACL
|
||||
# ------------------------------------------------------------------------
|
||||
ssh "$host" 'sudo install -d -o root -g root -m 0755 /etc/tireless'
|
||||
ssh "$host" 'sudo install -d -o tireless -g tireless -m 0750 /var/lib/tireless || \
|
||||
echo "tireless user not created yet — first deploy runs systemd-sysusers"'
|
||||
# /var/lib/tireless is deliberately NOT created here: the service account
|
||||
# does not exist until the first deploy runs systemd-sysusers, so this always
|
||||
# raced. The units declare StateDirectory=tireless, which makes systemd create,
|
||||
# own and label it at first start.
|
||||
|
||||
# The service account needs to read the host key for mTLS to Postgres (§11).
|
||||
ssh "$host" 'sudo setfacl -m u:tireless:r "/etc/pki/tls/private/$(hostname -f).pem" || \
|
||||
@@ -295,7 +297,8 @@ Remaining one-time steps (operator, on the target host):
|
||||
# then: /login, and complete the browser flow
|
||||
|
||||
This writes /var/lib/tireless/.claude.json. The token refreshes in place,
|
||||
which is why the unit grants ReadWritePaths=/var/lib/tireless.
|
||||
which is why the unit declares StateDirectory=tireless (systemd creates
|
||||
it, owns it as the service account, and keeps it writable).
|
||||
|
||||
Skip this only if you intend to run pay-as-you-go, in which case put
|
||||
ANTHROPIC_API_KEY in /etc/tireless/tireless.env instead. Do not do both:
|
||||
|
||||
Reference in New Issue
Block a user