Found while reviewing this pipeline against the new fleet hugo type (ptr727/ProjectTemplate#560), which asserts hugo.deploy.retention. This is the one finding with an outage attached, so it is worth reading before the other two.
The gap
deploy/make-release.sh prunes correctly and even asserts the result, which is what makes this easy to miss:
# deploy/make-release.sh:212-230
echo "==> pruning to the newest $KEEP_RELEASES releases"
mapfile -t all < <(find "$ROOT/releases" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort)
target="$(basename "$(readlink "$ROOT/current")")"
...
remaining=$(find "$ROOT/releases" -mindepth 1 -maxdepth 1 -type d | wc -l)
if [ "$remaining" -gt "$KEEP_RELEASES" ]; then
echo "prune failed: $remaining releases remain, expected at most $KEEP_RELEASES" >&2
exit 1
fi
But $ROOT in CI is the scratch bundle, not the host:
# .github/workflows/deploy-site-task.yml:89
deploy/make-release.sh "${RUNNER_TEMP}/bundle" "${{ steps.release.outputs.id }}"
${RUNNER_TEMP}/bundle is a fresh tree holding exactly one release, so remaining is always 1, the assertion passes trivially, and the prune has never touched the deploy host. grep -n 'ssh |prune|rm -rf' .github/workflows/deploy-site-task.yml returns only the two -e "ssh ..." transport flags. There is no remote prune step anywhere in the deploy path.
The upload deliberately carries no --delete, which is correct (at an environment root it would remove the rollback targets), so nothing else reclaims the space either. OPERATIONS.md:162-166 describes the ten-release policy against the local-mirror path, which is accurate for that path and reads as coverage for this one.
Why it matters
<environment>/releases/ grows by one full release per deploy, forever. --link-dest keeps the marginal cost low while releases stay similar, but nothing bounds the count, and the failure surfaces as the site going down when the disk fills rather than as a red deploy. That is the exact shape the assertion in make-release.sh was written to prevent, applied to the wrong tree.
The fix
A step after the pointer flip that prunes <environment>/releases/ on the host and asserts the count converged, failing the deploy when it does not. Two constraints worth carrying over from the local implementation, both already right there:
- The release
current resolves to is never a prune candidate, whatever the sort order says.
- The assertion is the point. A prune whose pattern stops matching reports success while the disk fills, which is the same failure class as a gate that stops gating.
Worth noting the deploy key is rrsync -wo confined, so a remote prune needs either a second forced command or a host-side timer. A host-side timer is the option that keeps the deploy key with no delete capability at all, at the cost of the deploy no longer being able to assert the outcome. If the timer is preferred, the deploy should still read the count back and fail on divergence, so the assertion survives.
The fleet reference leaf (catalog/snippets/workflows/deploy-site-task.yml in ptr727/ProjectTemplate#560) carries this step, so it currently leads this repo by one.
Recorded in the hub registry as a driftNote against hugo.deploy.retention until it clears.
Found while reviewing this pipeline against the new fleet
hugotype (ptr727/ProjectTemplate#560), which assertshugo.deploy.retention. This is the one finding with an outage attached, so it is worth reading before the other two.The gap
deploy/make-release.shprunes correctly and even asserts the result, which is what makes this easy to miss:But
$ROOTin CI is the scratch bundle, not the host:${RUNNER_TEMP}/bundleis a fresh tree holding exactly one release, soremainingis always 1, the assertion passes trivially, and the prune has never touched the deploy host.grep -n 'ssh |prune|rm -rf' .github/workflows/deploy-site-task.ymlreturns only the two-e "ssh ..."transport flags. There is no remote prune step anywhere in the deploy path.The upload deliberately carries no
--delete, which is correct (at an environment root it would remove the rollback targets), so nothing else reclaims the space either.OPERATIONS.md:162-166describes the ten-release policy against the local-mirror path, which is accurate for that path and reads as coverage for this one.Why it matters
<environment>/releases/grows by one full release per deploy, forever.--link-destkeeps the marginal cost low while releases stay similar, but nothing bounds the count, and the failure surfaces as the site going down when the disk fills rather than as a red deploy. That is the exact shape the assertion inmake-release.shwas written to prevent, applied to the wrong tree.The fix
A step after the pointer flip that prunes
<environment>/releases/on the host and asserts the count converged, failing the deploy when it does not. Two constraints worth carrying over from the local implementation, both already right there:currentresolves to is never a prune candidate, whatever the sort order says.Worth noting the deploy key is
rrsync -woconfined, so a remote prune needs either a second forced command or a host-side timer. A host-side timer is the option that keeps the deploy key with no delete capability at all, at the cost of the deploy no longer being able to assert the outcome. If the timer is preferred, the deploy should still read the count back and fail on divergence, so the assertion survives.The fleet reference leaf (
catalog/snippets/workflows/deploy-site-task.ymlin ptr727/ProjectTemplate#560) carries this step, so it currently leads this repo by one.Recorded in the hub registry as a driftNote against
hugo.deploy.retentionuntil it clears.