Repository navigation
Run a component's release command once per deploy (#119) - #135
Conversation
a9ac967 to
fde9bfd
Compare
|
@romamo Decided without waiting: for a component with Why: S-005-2, S-005-4 and S-005-6 need the command to run once, before any Reply with a comment starting with |
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>
fde9bfd to
213203d
Compare
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.jsongainsspec.release:command(non-empty argv), optionalworkingDirectory(relative, no.., therepository.subdirectoryrule), optionaltimeoutSeconds(1 to 1800, default 300). The SDK parses it into value objects (CommandArgv,RelativeDirectory,ReleaseCommandTimeout,ComponentRelease); the release server isdeployment.servers[0]release(with itsserver) only for components that declare one, plus an emptycloudfall_release_serversgroupdeploy.ymlis now three plays: a localhost play adds the deployed component's release server tocloudfall_release_servers; that server deploys first in its ownserial: 1,any_errors_fatalplay; the remaining servers follow one at a time as before. A failed release-server play stops the playbook, so no other server is touchedcloudfall_deployrole runstasks/release_command.ymlright afterMaterialize locked dependencies for the release, on the release server only. It goes throughsystemd-run --wait --pipe --collectas the application user, in the release directory (orworkingDirectory), with--property=EnvironmentFile=-<installRoot>/shared/env/<component>.env(the unit's own line),RuntimeMaxSec=<timeoutSeconds>, andNoNewPrivileges/PrivateTmplike the unit. A relativecommand[0]resolves against the release directory, as the unit resolvesservice.commandagainstcurrent. stderr is folded into stdout sooutputTailis the combined outputFinished 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 beforeInstall the managed service unitand the switch, so nothing changes on any server and no receipt is written. The SDK already turns this intoENGINE_STEP_FAILEDspec.releaseCommand(server,command,workingDirectory,exitCode: 0,outputTailcapped at 2000). The receipt schema gains it as an optional propertyreleaseruns no new role task and writes the same receiptFixes #119
Test plan
engine/tests/test_release_command.py:ansible-playbookon the controller, withcloudfall_deploy_systemd_runpointing at a stand-in that honours--working-directoryand 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 mergedeploy.ymlruns for real against two local hosts withdeployment.servers: [h2, h1](inventory order h1, h2), with the role swapped through--rolesfor a recorder. The release server goes first and runs the command once, a failure there stops h1, and a component withoutreleasekeeps inventory orderwhenconditions, and the receipt againstrelease-receipt.schema.jsonsdk/tests/test_validation.py:config validateexitsCONFIG_INVALIDfor an absolute or..working directory, an empty command or argument, a missing command, and timeouts 0 and 1801. It accepts the valid shapessdk/tests/test_inventory.py,engine/tests/test_ansible_inventory.py: value-object bounds, release server = first deployment server, renderedreleaseblock, noreleasekey for components without oneuv run ruff check .,uv run mypy,ansible-lint engine/ansible(withengine/ansible/ansible.cfg),uv run cloudfall config validate --project config/examples, thedeploy.ymlsyntax check, and the tests of the touched files (124 passed). The full suite is left to CIsystemd-run(its exit-code propagation, theFinished with result: timeoutline,EnvironmentFile=quoting), an actual table created by a migration, andcurrent/unit/restart state on real serversPlan
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