Repository navigation
Run Component build steps in artifact build (S-002) - #138
Conversation
|
@romamo Decided without waiting: a build step that deletes a file git tracks fails the build with Why: S-002-3 says the tarball holds every file Reply with a comment starting with |
|
@romamo Decided without waiting: artifact metadata gains an optional Why: S-002-4..8 ask for these; a Component without Reply with a comment starting with |
artifact build packaged the git tree only, so a Component could not ship a built frontend such as a gitignored web/dist. It now checks every spec.buildTools entry on PATH at its minimum version before cloning, runs each spec.build step in the clean checkout through env -i with only PATH, HOME, LANG, TMPDIR and CI=true, streams step output to stderr so stdout stays one JSON envelope, and packages the tracked files plus every spec.artifact.include path. A failed or timed-out step or a missing include writes no archive. The Artifact metadata gains an optional spec.build with the steps and tool versions. A Component without build is packaged exactly as before. Spec 002 is now built: every criterion has its test and its verification. Fixes #115 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…acked files A fixed 1800 s command limit cut long builds short with treaty's TIMEOUT, and the SDK/MCP path capped the engine call at 3600 s, so a step never reached its own timeoutSeconds. artifact build now declares no fixed limit and applies its own: 1800 s for the clone plus every declared step timeout (ComponentInventory.artifact_timeout_seconds), shared by every child, so a step's own limit is the one that fires (artifact_build_step_failed). An explicit --timeout still replaces it. The SDK's artifact step carries no cap of its own and leaves the limit to the engine. A step that deletes a tracked file broke S-002-3 by leaving it out of the archive. The build now refuses it with artifact_tracked_file_missing, naming the path, before any archive is written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Treaty's --timeout reaches a handler only as the effective timeout, the same whether the flag was omitted or 0, so the computed limit silently replaced an explicit 0. The command now takes --timeout as a field of its own, which treaty allows in place of its framework flag, parsed with Timeout.parse: omitted applies the clone time plus every declared step timeout, 0 disables the limit, and any other value replaces it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mlink A tracked symlink above an include path (web/link -> /outside, include web/link/secrets) let the build package controller files outside the checkout. The include path itself may still be a symlink, archived as one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e668682 to
65b1670
Compare
Summary
Implements S-002's build steps (
docs/specs/002-component-build-steps.md), criteria S-002-2 to S-002-10, S-002-12 and S-002-13. #113 shipped validation (S-002-1, S-002-11) and #114 the runtime refusal (S-002-14, S-002-15), so this is the spec's last build issue: the spec is nowstatus: built, with its Verification section filled in.cloudfall-engine artifact buildon a Component withspec.build:spec.buildToolsentry must be on the controller'sPATH(artifact_build_tool_missing) and<tool> --versionmust print a version at least the minimum (artifact_build_tool_unsupported, also when no version can be read).envis looked up the same waygit ls-filesunder the packaged root is read, then each step runs in order in<packaged root>/<workingDirectory>throughctx.run(..., stream="always")asenv -i PATH=… HOME=… LANG=… TMPDIR=… CI=true <tool path> <args>. The step sees only those variables, soHCLOUD_TOKEN, SOPS keys and the rest stay out. Its output goes to stderr as plain text and never to stdout. A non-zero exit, or the step outrunning itstimeoutSeconds, fails withartifact_build_step_failed, naming the step index, argv, exit code and stderr tailartifact.includepath must exist afterwards (artifact_include_missing). Every tracked file must still exist too: a step that deletes one is refused withartifact_tracked_file_missingnaming the path, since S-002-3 requires the tarball to hold every filegit ls-fileslistsnode_modules/and caches stay outBuildBudget) is 1800 s for the clone plus every declared steptimeoutSeconds(ComponentInventory.artifact_timeout_seconds), shared by every git,--versionand step child. A step's own limit is therefore the one that fires, asartifact_build_step_failed. An explicit--timeoutreplaces the budget, and running past it ends inTIMEOUT.--timeout 0still means no limit, as on main. Treaty's own--timeoutreaches the handler only as the effectivectx.timeout/ctx.remaining, which read the same whether the flag was omitted or 0 (Timeout.parsemaps 0 toTimeout(None)). So the command takes--timeoutas a field of its own (BuildArgs.timeout, parsed with treaty'sTimeout.parse), which treaty documents as replacing the framework flag (REQ-C-012). On the SDK/MCP path (build_release_artifact), the engine call now carries no 3600 sSTEP_TIMEOUT_SECONDScap (ExecutionStep.timeout_seconds=None), so the engine's budget governs there too.tar.gzand no.json.ArtifactBuildErrornow carries acontext(tool, found/minimum, step, argv, exitCode, path), which the CLI merges intoerror.contextnext tocodeartifact.schema.jsongains an optionalspec.build:steps(workingDirectory,command) andtools(name to the dotted version read in step 1). This change only adds fieldsbuildgoes down the unchanged code path: same_write_archivetree, nols-files, nospec.buildartifact buildcommand declareschild_log=True, and its description and thebuild_artifactMCP tool's description mention the build.engine/README.mddocuments the behaviourFixes #115
Test plan
engine/tests/test_artifact_build_steps.pyrunsartifact buildas a child process with its own environment.PATHholds only a temp dir containing a logginggitwrapper,env, and Python stand-ins fornodeandpnpm, so CI needs neither tool installed.HCLOUD_TOKENandSOPS_AGE_KEYare set for the command:web/dist/index.htmldespite.gitignore, holds every tracked file at the commit, and has nonode_modulesspec.buildrecords the steps and versions, and the file validates against the schemaclonereaches the git wrapperweb/package.jsonfails withartifact_tracked_file_missingand writes nothingtimeoutSeconds(artifact_build_step_failed). Three 3600 s steps give a budget of 1800 + 10800 s, and each step gets its full hour.--timeout 3ends inTIMEOUT.--timeout 0applies no limit and the build succeeds. With no--timeout, the computed limit applies.sdk/tests/test_lifecycle.pyshows the SDK's engine step carries no cap of its ownCI=trueand no secretscloudfall_deployrole's own path, digest, unpack and switch tasks run underansible-playbookagainst a controller directory, followed byrollback.yml's switch tasks. Ownership is dropped and the systemd tasks are skipped.releases/<release>/web/dist/index.htmlholds each page, andcurrent/web/dist/follows the deploy and the rollback. Ansible'sunarchiveneeds GNU tar, so the test is skipped on macOS and runs on CI's Linux runner. Locally the same flow passed with the unpack swapped fortar -xzfengine/tests/test_artifact_builder.py: the existing direct calls pass the newrun_stepandenvironmentargumentsuv run ruff check .,uv run mypy,uv run cloudfall config validate --project config/examples,specs.py check,specs.py coverage --spec 002(every criterion has a test), and the tests of the touched areas:test_artifact_builder.py,test_artifact_build_steps.py,test_playbook.py,test_release_command.py,test_agent_server.py,test_lifecycle.py, plustest_inventory.py,test_data_migration.py,test_migrate.py,test_secrets.py,test_domains_converge.py,test_app.pyandtest_component_build_validation.pyafter the timeout change. All passed except one skip, the S-002-12 GNU tar case. No Ansible file changed. The full suite is left to CInode/pnpmbuild, and a deploy and rollback on a live Debian/systemd hostPlan
spec.buildmetadata and schema, then tests per criterion (see the spec's Behaviour)specs.py coverage --spec 002)🤖 Generated with Claude Code