Skip to content

Run a component's release command once per deploy (#119) - #135

Merged
shipmill-romamo[bot] merged 2 commits into
mainfrom
feat/119-release-command
Oct 27, 2026
Merged

shipmill-romamo[bot] merged 2 commits into
mainfrom
feat/119-release-command

Conversation

@shipmill-romamo

Copy link
Copy Markdown
Contributor

Summary

Implements S-005's release command (docs/specs/005-release-command.md), criteria S-005-1 to S-005-7, S-005-10 and S-005-11. The plan and rollback parts (S-005-8, S-005-9) are #120.

  • component.schema.json gains spec.release: command (non-empty argv), optional workingDirectory (relative, no .., the repository.subdirectory rule), optional timeoutSeconds (1 to 1800, default 300). The SDK parses it into value objects (CommandArgv, RelativeDirectory, ReleaseCommandTimeout, ComponentRelease); the release server is deployment.servers[0]
  • The rendered inventory carries release (with its server) only for components that declare one, plus an empty cloudfall_release_servers group
  • deploy.yml is now three plays: a localhost play adds the deployed component's release server to cloudfall_release_servers; that server deploys first in its own serial: 1, any_errors_fatal play; the remaining servers follow one at a time as before. A failed release-server play stops the playbook, so no other server is touched
  • The cloudfall_deploy role runs tasks/release_command.yml right after Materialize locked dependencies for the release, on the release server only. It goes through systemd-run --wait --pipe --collect as the application user, in the release directory (or workingDirectory), with --property=EnvironmentFile=-<installRoot>/shared/env/<component>.env (the unit's own line), RuntimeMaxSec=<timeoutSeconds>, and NoNewPrivileges/PrivateTmp like the unit. A relative command[0] resolves against the release directory, as the unit resolves service.command against current. stderr is folded into stdout so outputTail is the combined output
  • A non-zero exit, or systemd's Finished with result: timeout, fails the play with a message naming the command, the exit code or the timeout, and the last 500 characters of output. That happens before Install the managed service unit and the switch, so nothing changes on any server and no receipt is written. The SDK already turns this into ENGINE_STEP_FAILED
  • Every server checks that the release server's run passed before it switches. Each receipt it writes carries that run as spec.releaseCommand (server, command, workingDirectory, exitCode: 0, outputTail capped at 2000). The receipt schema gains it as an optional property
  • A component without release runs no new role task and writes the same receipt

Fixes #119

Test plan

  • engine/tests/test_release_command.py:
    • the role's release-command tasks run for real under ansible-playbook on the controller, with cloudfall_deploy_systemd_run pointing at a stand-in that honours --working-directory and runs the argv after --. This covers working directory, relative executable, uid/gid, EnvironmentFile line equal to the unit template's, exit 1 message, timeout message, 2000-character tail and stderr merge
    • deploy.yml runs for real against two local hosts with deployment.servers: [h2, h1] (inventory order h1, h2), with the role swapped through --roles for a recorder. The release server goes first and runs the command once, a failure there stops h1, and a component without release keeps inventory order
    • Templar replays cover task order, the when conditions, and the receipt against release-receipt.schema.json
  • sdk/tests/test_validation.py: config validate exits CONFIG_INVALID for an absolute or .. working directory, an empty command or argument, a missing command, and timeouts 0 and 1801. It accepts the valid shapes
  • sdk/tests/test_inventory.py, engine/tests/test_ansible_inventory.py: value-object bounds, release server = first deployment server, rendered release block, no release key for components without one
  • Ran locally: uv run ruff check ., uv run mypy, ansible-lint engine/ansible (with engine/ansible/ansible.cfg), uv run cloudfall config validate --project config/examples, the deploy.yml syntax check, and the tests of the touched files (124 passed). The full suite is left to CI
  • Not run on a live Debian/systemd host: the real systemd-run (its exit-code propagation, the Finished with result: timeout line, EnvironmentFile= quoting), an actual table created by a migration, and current/unit/restart state on real servers

Plan

  • Serves: quality bar
  • Files: config/schemas/v1/component.schema.json, config/schemas/v1/release-receipt.schema.json, sdk/src/cloudfall/domain.py, sdk/src/cloudfall/inventory.py, engine/src/cloudfall_engine/ansible_inventory.py, engine/ansible/playbooks/deploy.yml, engine/ansible/roles/cloudfall_deploy/{defaults,meta,tasks}, engine/README.md, changelog.d/119-release-command.md, tests
  • Order: schema and value objects, inventory render, release-command tasks and receipt in the role, release-server-first plays in deploy.yml, tests per criterion (see the spec's Behaviour)
  • Proof: S-005-1, S-005-2, S-005-3, S-005-4, S-005-5, S-005-6, S-005-7, S-005-10, S-005-11 (specs.py coverage --spec 005; S-005-8 and S-005-9 stay with Show the release command in the deploy and rollback plans (S-005-8, S-005-9) #120)

🤖 Generated with Claude Code

@shipmill-romamo

Copy link
Copy Markdown
Contributor Author

@romamo Decided without waiting: for a component with release, deploy.yml deploys the release server first in its own play, through a new inventory group cloudfall_release_servers; the receipt's releaseCommand.outputTail holds stdout and stderr together.

Why: S-005-2, S-005-4 and S-005-6 need the command to run once, before any current moves; a play of its own is how Ansible gives that order. A component without release keeps today's server order and tasks, and an inventory without the new group only warns. Review made a failing systemd-run (an unknown user, a unit name in use) report its own error instead of an empty "exited with code 1".
Not taken: running the command from the controller over SSH before the play, which would bypass the unit's user and env file that S-005-3 asks for.

Reply with a comment starting with veto before 2026-10-28 08:45 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 27, 2026
shipmill-romamo Bot and others added 2 commits October 27, 2026 10:48
A component can declare release.command (argv, optional workingDirectory,
timeoutSeconds 1..1800, default 300). The deploy runs it on the first
server of deployment.servers, after the release's locked dependencies are
materialized and before the unit file changes or current moves, in a
transient systemd unit as the application user with the unit's own
EnvironmentFile= line, so migrations no longer run by hand after the gate.

deploy.yml now deploys that release server first, in its own play, so the
command runs once and before any server switches; a non-zero exit or a
timeout fails that play and the playbook stops before any other server.
Every other server asserts the command passed before it switches, and every
receipt of the deploy carries the release server's releaseCommand. A
component without release runs no new task and writes the same receipt.

Fixes #119

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A unit that never starts (it already exists, an unknown user) left the
failure message with empty command output and no reason. The message now
ends with the tail of systemd-run's stderr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shipmill-romamo
shipmill-romamo Bot force-pushed the feat/119-release-command branch from fde9bfd to 213203d Compare October 27, 2026 08:53
@shipmill-romamo
shipmill-romamo Bot merged commit 54995c8 into main Oct 27, 2026
1 check passed
@shipmill-romamo
shipmill-romamo Bot deleted the feat/119-release-command branch October 27, 2026 08:57
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.

Run a component's release command before the deploy switches current (S-005-1..7, 10, 11)

0 participants