fix(core): the per-key manifest lock did not actually exclude
deploy / build (push) Waiting to run
deploy / build-web (push) Failing after 55s
deploy / deploy (push) Has been cancelled
deploy / deploy-web (push) Has been cancelled

The lock added alongside compare-and-set removed its map entry inside
the critical section. A queued waiter still held that mutex, so the next
arrival found no entry, created a fresh one, and the two then ran
concurrently -- defeating the lock exactly when there was contention,
which is the only time it does anything.

I previously reported this fix as working on the strength of ten clean
release-mode runs. That was wrong: in debug, where the timing differs,
the guarding test fails 3 times in 12. Retiring the entry only after
dropping our own reference, under the same shard lock `entry` takes,
gives 0 failures in 25 -- and restoring the old behaviour reproduces 3
in 12, so the test really does catch it.

The consequence was the one the test names: a file served to a client
but never recorded, and so invisible the moment the instance is sealed.
Real against the local filesystem store, which has no conditional put
and relies on the verify-after-write retry; MinIO's compare-and-set
covers the same race, which is why the deployed path never showed it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64
This commit is contained in:
2026-08-31 13:03:55 +03:00
co-authored by Claude Opus 5
parent 5a94d90b79
commit 3790f2589c
19 changed files with 758 additions and 80 deletions
+92 -12
View File
@@ -13,8 +13,14 @@
# What it does:
# 1. bob — gitea_ci user, runner key, scoped sudoers, SELinux port label
# 2. caveman — the rustingface bucket and a scoped MinIO service account
# 3. hanzalova— rf.internal cert, nginx vhost, webroot, step@ renewal timer,
# and the gitea_ci account CI uses to ship the built frontend
# 3. hanzalova— rf.internal cert (internal CA) and rustingface.com cert
# (Let's Encrypt), both vhosts, the webroot, the renewal
# timers, and the gitea_ci account CI uses to ship the frontend
#
# Public DNS is NOT touched here. The Cloudflare token can rewrite DNS for
# every zone on the account, including MX records for domains whose mail we
# host (public-dns.md §1), so publishing the name is a separate, deliberate
# operator step. This script prints the exact call and verifies the result.
#
# Application config is NOT shipped here. The deploy workflow renders it from
# Gitea secrets on every deploy.
@@ -30,6 +36,9 @@ readonly MINIO_URL=http://caveman.kosherinata.internal:9000
readonly BUCKET=rustingface
readonly VHOST=rf
readonly WEBROOT=/var/www/rustingface
readonly PUBLIC_NAME=rustingface.com
readonly SITE_INDIRECTION=nh.thgttg.com # hanzalova's WAN address
readonly CERTBOT_CREDENTIALS=/root/.certbot-internal
readonly RUNNER_PUBKEY="${RUNNER_PUBKEY:-$HOME/.ssh/id_gitea_ci.pub}"
readonly PROVISIONER_PASSWORD="${PROVISIONER_PASSWORD:-$HOME/.step/secrets/provisioner}"
readonly ROOT_CA=/etc/pki/ca-trust/source/anchors/root-internal.pem
@@ -107,13 +116,14 @@ gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/${APP}/config.toml
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/${APP}/s3-access-key
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/${APP}/s3-secret-key
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/${APP}/hf-token
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/${APP}/client-tokens
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/systemd/system/${APP}.service
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/sysusers.d/${APP}.conf
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/firewalld/services/${APP}.xml
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemd-sysusers
gitea_ci ALL=(root) NOPASSWD: /usr/bin/install -d -o root -g ${APP} -m 0750 /etc/${APP}
gitea_ci ALL=(root) NOPASSWD: /usr/bin/chown -R root\:${APP} /etc/${APP}
gitea_ci ALL=(root) NOPASSWD: /usr/bin/chmod 0640 /etc/${APP}/config.toml /etc/${APP}/s3-access-key /etc/${APP}/s3-secret-key /etc/${APP}/hf-token
gitea_ci ALL=(root) NOPASSWD: /usr/bin/chmod 0640 /etc/${APP}/config.toml /etc/${APP}/s3-access-key /etc/${APP}/s3-secret-key /etc/${APP}/hf-token /etc/${APP}/client-tokens
gitea_ci ALL=(root) NOPASSWD: /usr/bin/chmod 0755 /usr/local/bin/${APP}
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/restorecon -R /usr/local/bin/${APP} /etc/${APP}
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl daemon-reload
@@ -242,19 +252,61 @@ provision_proxy() {
ssh "$PROXY_HOST" "sudo systemctl enable --now step@${VHOST}.timer"
ok "renewal timer enabled"
# The vhost references the cert paths, so it is only installed after the
# cert exists: nginx -t fails on a missing ssl_certificate and that blocks
# every unrelated reload on the host.
# The public cert. Idempotent: --keep-until-expiring makes a repeat run a
# no-op while the lineage is still valid. DNS-01 means this works before
# the name resolves anywhere, so the cert can exist before the record does.
local have_public
have_public=$(ssh "$PROXY_HOST" \
"sudo test -d /etc/letsencrypt/live/${PUBLIC_NAME} && echo yes || echo no" | tail -1)
if [ "$have_public" = no ]; then
if ! ssh "$PROXY_HOST" "sudo test -f ${CERTBOT_CREDENTIALS}"; then
die "${CERTBOT_CREDENTIALS} is missing on ${PROXY_HOST}; cannot issue ${PUBLIC_NAME}"
fi
ssh "$PROXY_HOST" "sudo certbot certonly \
-m ops@${PUBLIC_NAME} --agree-tos --no-eff-email --noninteractive \
--cert-name ${PUBLIC_NAME} \
--key-type ecdsa \
--dns-cloudflare \
--dns-cloudflare-credentials ${CERTBOT_CREDENTIALS} \
--dns-cloudflare-propagation-seconds 60 \
--keep-until-expiring \
-d ${PUBLIC_NAME} -d www.${PUBLIC_NAME}"
ok "issued ${PUBLIC_NAME}"
else
ok "${PUBLIC_NAME} cert is present"
fi
# certbot renews on its own timer, but a reload is only a request and a
# failed one leaves the old cert on the wire for up to 90 days. The hook
# must therefore leave a trace rather than swallow the failure.
ssh "$PROXY_HOST" 'sudo bash -s' <<'REMOTE'
set -euo pipefail
install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'HOOK'
#!/bin/sh
systemctl reload nginx || logger -t reload-nginx -p daemon.err \
"nginx reload failed after certbot renewal; cert on disk is newer than the one served"
HOOK
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
REMOTE
ok "certbot deploy hook installed"
# The vhosts reference cert paths, so they are only installed once both
# certs exist: nginx -t fails on a missing ssl_certificate, and that
# blocks every unrelated reload on this shared proxy.
rsync -az --rsync-path='sudo rsync' --mkpath \
asset/nginx/${VHOST}.internal.conf \
"$PROXY_HOST:/etc/nginx/sites-available/${VHOST}.internal.conf"
asset/nginx/rustingface-upstream.conf \
"$PROXY_HOST:/etc/nginx/conf.d/rustingface-upstream.conf"
for conf in ${VHOST}.internal.conf ${PUBLIC_NAME}.conf; do
rsync -az --rsync-path='sudo rsync' --mkpath \
"asset/nginx/${conf}" "$PROXY_HOST:/etc/nginx/sites-available/${conf}"
ssh "$PROXY_HOST" "sudo ln -sfn ../sites-available/${conf} /etc/nginx/sites-enabled/${conf}"
done
ssh "$PROXY_HOST" "
set -euo pipefail
sudo ln -sfn ../sites-available/${VHOST}.internal.conf \
/etc/nginx/sites-enabled/${VHOST}.internal.conf
sudo nginx -t
sudo systemctl reload nginx"
ok "vhost installed and nginx reloaded"
ok "both vhosts installed and nginx reloaded"
# A reload is a request, not a guarantee: nginx keeps its old cycle if it
# cannot rebind, and then serves the certificate it loaded then. Compare
@@ -320,6 +372,10 @@ gitea_ci ALL=(root) NOPASSWD: /usr/bin/install -d -o root -g root -m 0755 ${WEBR
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/restorecon -R ${WEBROOT}
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/nginx -t
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl reload nginx
# The deploy's health check asks whether the public certificate exists
# before probing the public vhost; /etc/letsencrypt/live is root-only, so
# an unprivileged test silently returns false and would skip the check.
gitea_ci ALL=(root) NOPASSWD: /usr/bin/test -d /etc/letsencrypt/live/rustingface.com
SUDOERS
chmod 0440 /etc/sudoers.d/.${APP}_gitea_ci.new
visudo -cf /etc/sudoers.d/.${APP}_gitea_ci.new
@@ -327,7 +383,31 @@ mv /etc/sudoers.d/.${APP}_gitea_ci.new /etc/sudoers.d/${APP}_gitea_ci
REMOTE
ok "webroot ${WEBROOT} and scoped sudoers on the proxy"
info "DNS"
info "public DNS (${PUBLIC_NAME})"
if getent hosts "${PUBLIC_NAME}" >/dev/null; then
ok "${PUBLIC_NAME} resolves to $(getent hosts "${PUBLIC_NAME}" | awk '{print $1}' | head -1)"
else
echo " ${PUBLIC_NAME} does not resolve yet. It is a zone apex, so it takes an"
echo " A record rather than a CNAME (public-dns.md §3). From ${PROXY_HOST}:"
echo
echo " TOKEN=\$(sudo grep -oP '(?<=dns_cloudflare_api_token\s=\s).*' \\"
echo " ${CERTBOT_CREDENTIALS} | tr -d \"'\" \")"
echo " zid=\$(curl -sS -H \"Authorization: Bearer \$TOKEN\" \\"
echo " 'https://api.cloudflare.com/client/v4/zones?name=${PUBLIC_NAME}' | jq -r '.result[0].id')"
echo " # apex: A record to this site's WAN address"
echo " curl -sS -X POST \"https://api.cloudflare.com/client/v4/zones/\$zid/dns_records\" \\"
echo " -H \"Authorization: Bearer \$TOKEN\" -H 'Content-Type: application/json' \\"
echo " --data '{\"type\":\"A\",\"name\":\"${PUBLIC_NAME}\",\"content\":\"$(dig +short ${SITE_INDIRECTION} A | head -1)\",\"ttl\":1,\"proxied\":false,\"comment\":\"rustingface\"}'"
echo " # www: CNAME to the site indirection, so a re-homed site is one record"
echo " curl -sS -X POST \"https://api.cloudflare.com/client/v4/zones/\$zid/dns_records\" \\"
echo " -H \"Authorization: Bearer \$TOKEN\" -H 'Content-Type: application/json' \\"
echo " --data '{\"type\":\"CNAME\",\"name\":\"www.${PUBLIC_NAME}\",\"content\":\"${SITE_INDIRECTION}\",\"ttl\":1,\"proxied\":false,\"comment\":\"rustingface\"}'"
echo
echo " Run these from ${PROXY_HOST}: the token is already there, and copying it"
echo " off the host spreads a credential that can edit every zone we own."
fi
info "mesh DNS"
if getent hosts "${VHOST}.internal" >/dev/null; then
ok "${VHOST}.internal resolves"
return 0