Skip to content

Keep a hand-made unit and restore it when a first release fails (#151) - #154

Merged
shipmill-romamo[bot] merged 2 commits into
mainfrom
fix/151-keep-hand-made-unit
Oct 28, 2026
Merged

shipmill-romamo[bot] merged 2 commits into
mainfrom
fix/151-keep-hand-made-unit

Conversation

@shipmill-romamo

Copy link
Copy Markdown
Contributor

Summary

A first deploy over a hand-made systemd unit of the same name (e.g. asrest.service, adopted on a brownfield move) no longer loses it when the release fails its health gate.

  • Before "Install the managed service unit", the deploy role reads the existing unit. When it is a regular file whose content does not start with # Managed by Cloudfall, it records systemctl is-enabled / is-active (exit codes read, never failing) and copies it with remote_src to /etc/systemd/system/<name>.service.pre-cloudfall, root:root 0644, force: false
  • In the rescue block, a failed first release over a kept unit is stopped as before, then the kept unit is copied back, systemd is reloaded, its enabled state is restored, and it is started when it was active. The fail message says the pre-Cloudfall unit was restored. Without a kept unit, the first-release path is unchanged, and so is the rollback-to-previous-release path
  • In check mode a debug task reports the unit a deploy would keep; nothing is copied
  • engine/README.md describes the new behaviour; changelog fragment changelog.d/151-keep-hand-made-unit.md

Tests

  • uv run ruff check ., uv run mypy, uv run pytest engine/tests/test_deploy_role.py engine/tests/test_release_command.py pass
  • The new tests in engine/tests/test_deploy_role.py replay the role's decisions with Ansible's own templating, as the existing tests there do. They cover: hand-made vs managed vs missing unit detection, which adoption tasks run normally and in check mode, the copy settings, the stale-copy refusal, which rescue steps run (restore and start, restore and leave stopped, no resurrection over a managed unit, rollback unchanged, nothing in check mode), the enabled-state command, and the fail messages
  • There was no live systemd run: the tests do not deploy to a real host and do not observe the original unit back and active
  • ansible-lint with ANSIBLE_CONFIG was not run locally (permission denied in the session); CI runs it

Decisions for you

  1. New refusal: if the existing unit is hand-made and differs from a .pre-cloudfall copy already on disk, the deploy now fails before installing anything. With force: false the old copy would otherwise be restored over the newer unit
  2. Managed unit with no previous release (e.g. current removed by hand): an old .pre-cloudfall copy is not restored. Restore only happens when this run found a hand-made unit
  3. How the enabled state is restored: systemctl reenable if it was enabled, otherwise systemctl disable. This removes the multi-user.target.wants link the deploy added. Side effect: rarer states (static, indirect, enabled-runtime) end up disabled
  4. Only regular files count: a symlinked unit (such as a masked one) is not kept and is overwritten as today
  5. Restored file mode: the restored unit is root:root 0644, not the original file's mode or owner
  6. Leftovers after a failed first release: the current symlink and the release directory stay, as today
  7. Not checked: ansible-lint with the config, the CI syntax-check, and a real systemd run (reenable/disable on a static unit, daemon-reload with no unit name)

Fixes #151

🤖 Generated with Claude Code

The deploy role keeps a same-name unit without the Cloudfall header at
<name>.service.pre-cloudfall, records whether it was enabled and active,
and on a failed first release copies it back, restores its enabled state
and starts it when it was running. Check mode reports the unit it would
keep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The S-002-12 harness runs "Compute release paths" with a component that
declares no service, so templating the unit paths there failed the play.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shipmill-romamo

Copy link
Copy Markdown
Contributor Author

@romamo Decided without waiting: a first deploy keeps a hand-made unit at /etc/systemd/system/<name>.service.pre-cloudfall and restores it when the release fails its gate, and a deploy refuses to start when a hand-made unit differs from a kept copy already there.

Why: it fixes #151 additively; components whose unit Cloudfall already manages deploy exactly as before, and the refusal keeps the copy equal to the unit it replaced, so a restore never puts back an older unit. The enabled state comes back as enabled or disabled, so a unit in a rarer state such as static comes back disabled.
Not taken: overwriting an existing kept copy with the newer hand-made unit, which would lose the first one silently; or keeping the copy under the install root, which the application user can write and root later restores.

Reply with a comment starting with veto before 2026-10-29 11:54 UTC to reverse it, adding what you want instead if you like. A later veto still counts and gets a follow-up.

@shipmill-romamo shipmill-romamo Bot added the decided An agent decided at notify; the maintainer may veto label Oct 28, 2026
@shipmill-romamo
shipmill-romamo Bot merged commit 4c9e66f into main Oct 28, 2026
1 check passed
@shipmill-romamo
shipmill-romamo Bot deleted the fix/151-keep-hand-made-unit branch October 28, 2026 11:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

decided An agent decided at notify; the maintainer may veto

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A first deploy over a hand-made unit of the same name leaves the service stopped when the health gate fails

0 participants