Skip to content

The remote release tree is never pruned, so the deploy host grows without bound #27

Description

@ptr727

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions