Phase 13: auto-release cron on VmAllocation.expires_at #14

Open
opened 2026-05-21 23:25:40 +00:00 by mik-tf · 0 comments
Owner

Summary

Closes a Phase 12 correctness gap: under the quota purchase model, VmAllocation.expires_at is set to the plan's vm_quota_period_end so all VMs from a plan expire together, but no cron enforces it. Without this tick, expired-plan VMs stay flagged Active in the admin /allocations view + user /dashboard indefinitely after the plan period ends.

Landed as squash-merge f8f7638 on development.

What landed

  • New module crates/hero_onboarding_server/src/auto_release.rs (197 LOC, 10 pure-fn unit tests): find_expired_allocation_sids(rows, now_epoch) (pure) + run_tick(osis, provisioner) (async). Filter is status != Released && expires_at < now (strict < — exact boundary is grace-period, not expired). Provisioner::release() already sets status=Released + stamps released_at, so this module owns no state mutation. AlreadyReleased counts as rows_already_released (idempotent).

  • New HTTP route POST /admin/auto-release-now (admin-secret-gated; 503 if no provisioner registered). Returns AutoReleaseSummary JSON with rows_examined, rows_expired, rows_released, rows_already_released, errors.

  • New cron action hero_onboarding_auto_release_cron (1h default cadence; HERO_ONBOARDING_AUTO_RELEASE_CRON_INTERVAL_MS override). Mirrors the Phase 4/5/12 cron pattern: hero_onboarding_admin auto-release --once POSTs to the new route.

  • New admin CLI subcommand Cmd::AutoRelease (4 LOC, mirrors Cmd::DiscountLadder).

  • Runbook §3.9 (~130 LOC) documents the cron + manual invocation curl recipe + short-cadence override + what the cron does NOT do (refund / provisioner teardown beyond release / persistent history). §8 further reading link added.

  • Smoke scripts/smoke_auto_release.sh (~280 LOC, 19 checks): route shape, admin gating (missing/wrong/right secret), empty-DB tick, live un-expired allocations not touched, force-released row filtered out of scan, tick idempotency, AutoReleaseSummary field shape.

Acceptance gates

  • cargo test --workspace: 120/120 (+10 vs s2-014 baseline of 110; all 10 new pure-fn unit tests for the filter logic)
  • cargo fmt --check: clean
  • cargo clippy --workspace --all-targets -- -D warnings: clean
  • lab build --release --install --workspace: VICTORY 3/3 (build #17)
  • lab infocheck: 3/3 clean / 0 findings
  • scripts/smoke_auto_release.sh: 19/19 GREEN (new)
  • Regression: smoke_discount_ladder.sh 24/24 + smoke_vm_allocate.sh 27/27

What this does NOT do (intentional deferrals)

  • Refund on auto-release — credits are not returned to balance when a plan expires. Same posture as /admin/release (D-17 §Revisability — ~10 LOC + design conversation about pro-rata + grief surface; deferred until ops asks).
  • End-to-end release-on-expire smoke — would require either waiting 30 days (the minimum positive plan duration), a new admin route to backdate VmAllocation.expires_at (operator-side test seam), or a now_epoch_override query param on the tick route (production-API smell). The 10 pure-fn unit tests cover the filter logic with crystal clarity; the live smoke proves the route wires up + the cron fires. Same gap-style as smoke_discount_ladder which similarly punts manipulate-clock-for-tier-promotion.

D-NN / L-NN

None minted — mechanical addition mirroring three existing cron patterns. Slots stay at D-22 / L-09.

Tracking context

This closes the last in-scope correctness gap before Track B archives back to single-agent Track A operation. The next provisioner integration (v1.5 DeployerProvisioner per home#235 Track D) is the natural-pause-point recovery hook; Track A's deployer arc is currently at D2 (Forge user lifecycle).

Meta: hero_onboarding#1.

Signed-by: mik-tf mik-tf@noreply.invalid

## Summary Closes a Phase 12 correctness gap: under the quota purchase model, `VmAllocation.expires_at` is set to the plan's `vm_quota_period_end` so all VMs from a plan expire together, but no cron enforces it. Without this tick, expired-plan VMs stay flagged `Active` in the admin `/allocations` view + user `/dashboard` indefinitely after the plan period ends. Landed as squash-merge [`f8f7638`](https://forge.ourworld.tf/lhumina_code/hero_onboarding/commit/f8f7638) on `development`. ## What landed - **New module** `crates/hero_onboarding_server/src/auto_release.rs` (197 LOC, 10 pure-fn unit tests): `find_expired_allocation_sids(rows, now_epoch)` (pure) + `run_tick(osis, provisioner)` (async). Filter is `status != Released && expires_at < now` (strict `<` — exact boundary is grace-period, not expired). `Provisioner::release()` already sets `status=Released` + stamps `released_at`, so this module owns no state mutation. `AlreadyReleased` counts as `rows_already_released` (idempotent). - **New HTTP route** `POST /admin/auto-release-now` (admin-secret-gated; 503 if no provisioner registered). Returns `AutoReleaseSummary` JSON with `rows_examined`, `rows_expired`, `rows_released`, `rows_already_released`, `errors`. - **New cron action** `hero_onboarding_auto_release_cron` (1h default cadence; `HERO_ONBOARDING_AUTO_RELEASE_CRON_INTERVAL_MS` override). Mirrors the Phase 4/5/12 cron pattern: `hero_onboarding_admin auto-release --once` POSTs to the new route. - **New admin CLI subcommand** `Cmd::AutoRelease` (4 LOC, mirrors `Cmd::DiscountLadder`). - **Runbook §3.9** (~130 LOC) documents the cron + manual invocation curl recipe + short-cadence override + what the cron does NOT do (refund / provisioner teardown beyond release / persistent history). §8 further reading link added. - **Smoke** `scripts/smoke_auto_release.sh` (~280 LOC, 19 checks): route shape, admin gating (missing/wrong/right secret), empty-DB tick, live un-expired allocations not touched, force-released row filtered out of scan, tick idempotency, `AutoReleaseSummary` field shape. ## Acceptance gates - `cargo test --workspace`: **120/120** (+10 vs s2-014 baseline of 110; all 10 new pure-fn unit tests for the filter logic) - `cargo fmt --check`: clean - `cargo clippy --workspace --all-targets -- -D warnings`: clean - `lab build --release --install --workspace`: VICTORY 3/3 (build #17) - `lab infocheck`: 3/3 clean / 0 findings - `scripts/smoke_auto_release.sh`: **19/19 GREEN** (new) - Regression: `smoke_discount_ladder.sh` 24/24 + `smoke_vm_allocate.sh` 27/27 ## What this does NOT do (intentional deferrals) - **Refund on auto-release** — credits are not returned to balance when a plan expires. Same posture as `/admin/release` (D-17 §Revisability — ~10 LOC + design conversation about pro-rata + grief surface; deferred until ops asks). - **End-to-end release-on-expire smoke** — would require either waiting 30 days (the minimum positive plan duration), a new admin route to backdate `VmAllocation.expires_at` (operator-side test seam), or a `now_epoch_override` query param on the tick route (production-API smell). The 10 pure-fn unit tests cover the filter logic with crystal clarity; the live smoke proves the route wires up + the cron fires. Same gap-style as `smoke_discount_ladder` which similarly punts manipulate-clock-for-tier-promotion. ## D-NN / L-NN None minted — mechanical addition mirroring three existing cron patterns. Slots stay at D-22 / L-09. ## Tracking context This closes the last in-scope correctness gap before Track B archives back to single-agent Track A operation. The next provisioner integration (v1.5 `DeployerProvisioner` per [home#235](https://forge.ourworld.tf/lhumina_code/home/issues/235) Track D) is the natural-pause-point recovery hook; Track A's deployer arc is currently at D2 (Forge user lifecycle). Meta: [hero_onboarding#1](https://forge.ourworld.tf/lhumina_code/hero_onboarding/issues/1). Signed-by: mik-tf <mik-tf@noreply.invalid>
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_onboarding#14
No description provided.