Changelog¶
All notable changes to this project are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]¶
[0.4.1] - 2026-10-11¶
Changed¶
- The
stevinstep waits for what another tool has yet to make, where the plan failed. stevin does not plan a table whose schema the bundle has not deployed yet, nor a table it only governs that a dlt pipeline has not loaded yet. On a target that was never deployed,lely planstopped at thestevinstep with stevin's refusal, and a new repository's first pull request got no plan: also not the plan that would have deployed the bundle and run the job. The step is now waiting: the plan succeeds, shows the steps above, and says why the tables wait.lely apply -t <target>runs the steps above, then plans the tables, shows them and asks: one command gets a new product up when nothing fails.lely apply plan.jsonstops at the tables with 2, and the next plan shows them. The same happens when the bundle later gets a new schema with tables in it. Any other failure ofstevin planfails the plan as before. Put thestevinstep below thebundlestep and below the step that runs the pipeline: lely does not check that. - The
stevinstep needs stevin 0.4.0a3 or newer, where it needed 0.4.0a2. It is the first stevin whoseplansays with an exit code that it needs what another tool has yet to make. A plan file an older stevin wrote fails the plan and names both versions. While something is missing an older stevin writes no file: the plan fails with stevin's own message, as it did. - A step that still waits when a run reaches it is no longer told to plan again.
The refusal says that no step above brought about what it waits for. A step
--frompasses over that could not be planned in full says so, where it said "planned".
Added¶
- The docs say how a plugin makes its step wait:
plananswers aStepPlanwhosewaitingsays why. lely holds such a step as it holds one that waits for an output. The field was there; what a run does with it was not written down.
[0.4.0] - 2026-10-11¶
Added¶
- The
stevinstep applies.lely applyhands stevin the plan file that was approved, not a new one, and passes--allow-destructiveon only when it was given. A plan stevin refuses as stale ends the run with 2 and says to plan again.lely statuslists the tables stevin's specs describe.lely destroyleaves them alone and says so: a dropped table is its rows. It has not run on a workspace yet. - The
stevinstep needs stevin 0.4.0a2 or newer. It reads stevin's exit codes to tell a refusal from a failure, and no longer stevin's words.lely statusasksstevin list, so it makes no plan. A plan from an older stevin fails and names both versions. -
A plugin can run what was reviewed. In
apply,ctx.approvedis the step's plan as it was approved, payload and all. A plugin without a destroy can say why innothing_to_destroy. -
lely.testinghascheck_no_stateandcheck_only_its_own, for the rules it had no check for: no state, only what its options name, only its own. Every check takes the plugin's class too, and then makes each call on a new instance, as lely does;checkon a step's own command line does so. The docs say what the kit cannot see: a write through the SDK or by another program. - A step on its own shows what it gives.
planprints the outputs it knows and names the ones that come later;applyprints what the step returned. ASecretis***. - A step on its own can plan its destroy:
plan --destroy -o gone.json, thendestroy --plan-file gone.json.
Changed¶
check --livesays what it is about to do before it does it: apply and destroy, on which target and workspace, and as whom. Without--live,checkno longer speaks of a destroy for a step that has none.- A flag takes
$${for a literal${, as a config file does. It was refused as a reference. lely doctoris no longer meant to say whether credentials are read-only. It never could. It says whether the identity is a workspace admin, and that it cannot tell what the credentials may change. For an admin it said "these credentials can change anything", which nobody had checked: that claim is gone.-
A plan made from
lely.ymlis accepted with the same project inpyproject.toml, whatever the spelling. What a step is made from is now each option as its plugin is handed it:1.10andyesin YAML are1.10andtruein TOML for a number or a yes or no, and"1.10"and"yes"for text. A plan file saved by 0.3.1 for a step with a number or a boolean in its options is refused once by the next release: the refusal names both versions and says to plan again. -
A secret written out is not in what the plan file keeps of a step's options. A
Secretoption given in the config, or as a flag of a step on its own, was part of the hash the plan file keeps of each step. Its text is left out now, and so is text written beside a reference in such an option. lely no longer notices that either changed between plan and apply; it never did for a value from the environment. - An output that holds what a
Secretoption was given is a secret, also when the step gives it as plain text. It was in the plan file and in-f jsonas it came. It is***and a marker now, and a step below can take it only into aSecretoption, and whole: no field of it. - The order of a mapping's entries counts. A plugin is handed them in the order written, so a plan file is refused when they were moved since.
- A plan file saved by 0.3.1 is refused once in three cases, and says to plan again.
A step has a
Secretoption that holds written-out text. A step has a mapping whose keys are not in sorted order, capitals first. Or a step takes an output that is a secret now and was text in the file: that one is refused when the run reaches the step, after the steps above it have run. -
A refusal for a value that is a secret on one side says neither value. It printed the one that was not a secret.
-
A stevin plan shows every step with its risk class, and a table that is rewritten says so in its own line. A risk class lely does not know is destructive; it was read as an update.
-
The README is short, and how to use lely is in the docs only. The docs' design page is now "How it works" and says what the code does today. The
spec/folder is gone: what must be true of lely is inRULES.md, and why inDECISIONS.md.
Fixed¶
- A flag for
Mapping[str, int](or a decimal number, or true and false) reads the value ofkey=valueas that type. Such a step started, and then refused every value. - A value from the environment is refused wherever it is no
Secret: in an option that takes anything (object), which took the secret into the plan, and as a list or a mapping given whole, whichlely validatelet through and the plan then refused. - A list or a mapping given whole by one reference is read by element. A
tuple[Secret, ...]got plain text and atuple[int, ...]got whatever came. Each element is now what the option declares, or an error. - The editors' schema takes a list or a mapping given whole by one reference, as
lely validatedoes:tags: ${steps.model.tags}is no longer marked as a mistake. Write the schema again withlely schema -o lely.schema.json.
[0.3.1] - 2026-10-10¶
Nothing lely plans or applies has changed. This is the first release from its new home.
Changed¶
- lely lives in
kostavo-oss/tools, one repository for the four Kostavo tools. The package, its name on PyPI and its command are what they were. The links on PyPI point at the new place, and the docs are atkostavo-oss.github.io/tools/lely/. From this release on a tag islely-v<version>. - The README says what lely will not do and when you no longer need it: the day a bundle plans its own before-and-after steps.
- The page has the look of the other Kostavo pages, and they have its. The colours and
what they mean, the type, a table, a badge and a block of code are now one style sheet
that caland's page and stevin's use too; lely's page was the reference, so it reads as
it did. Three things differ: a column's heading is a little less heavy, a
preor akbdin a plugin's view is drawn as the page's own, and what the keyboard is on has a blue ring. The page is still one file with its style sheet inline, no script, and nothing fetched.
[0.3.0] - 2026-10-08¶
A breaking release: the command plugin is gone, and a step runs on its own.
Added¶
- A step on its own (
spec/011). A step that inheritslely.step.Stepand ends withStep.main()is a command of its own:plan,apply,destroy,statusandcheck, its options as flags, with nolely.yml. A plan it writes is a one-step lely plan:lely apply plan.jsontakes it, and the same checks are made before anything runs.checkholds the step to the rules from the command line;--liveapplies and destroys on the target. Program: a program as a step, in ten lines — onerunevery apply, destructive if it says so, outputs from the JSON on its last line. Whatcommanddid, under the rules.
Removed¶
command: a program is aProgram(ten lines, see Writing a step); a config that namescommandis told bylely validatethat no plugin of that name is installed. → 011
[0.2.0] - 2026-10-07¶
Added¶
$${…}writes a literal${…}.apply: [sh, -c, 'echo $${HOME}']hands the shell${HOME}; before, there was no way to write one. The error for a${…}that is no reference says so, and names a step only when one above gives a value of that name.
Changed¶
- What a step's program says is shown while it runs.
bundle deploy,bundle destroy,bundle runand acommandstep's apply and destroy commands are passed on a line at a time, as they come, on stderr — what they write to stdout and to stderr. Before, a deploy or a long job run was silent until it ended, and a warning from a program that succeeded was never shown. - The source package holds the code, its tests and the files that say what it is. It also carried the docs site, the specs, the workflows and the lock file, and was five times the wheel's size. The wheel is as it was.
Removed¶
lely.step.StderrLogandlely.planfile.outputs_json: nothing used them.
Fixed¶
- A program that fails is quoted on both of its streams, each under its name. One that gave its reason on stdout and a notice on stderr was quoted for the notice alone.
- The README's quickstart can be followed: it shows the plugin file its config names. A
test checks every whole config in the README and the docs the way
lely validatedoes. lely doctorlooks for git, as the docs said it did: git missing or refusing the checkout, in a repository, is a failed check —lely planfails there. It compares the Databricks CLI's version with v1.3.0 and says when it is older, and says what bundles need only to a project with a step that runs the CLI.lely doctordoesn't fail for a Databricks CLI no step runs. In a project of commands a missing CLI is said, and is no failed check.- Why a run's record couldn't be written is shown, not obeyed: the error's own words went into the line as markup.
lely doctorsays a plugin that fails in one line, with the step and the plugin, and checks the rest. A plugin whoseprogramsraised ended it in a traceback.- A traceback never shows local values. With a typer below 0.23, an error lely didn't
expect printed every frame's locals, and the frames of
planandapplyhold the run's token and the environment. lely applywith neither a plan file nor-tis refused at once, with 2. It reached the workspace first, so without credentials it failed with 1 instead.- Every
--helpnames[tool.lely]again. It read "a pyproject.toml with . Found from here up": the brackets were taken for markup. So did whatlely schema -osays of apyproject.toml. - lely asks for a typer it works with:
typer>=0.17.5, where 0.1.0 said>=0.12. With an older one every--helpcould end in a traceback, or a command given no-tcould run without one. CI now runs the tests on the oldest version of every dependency.
[0.1.0] - 2026-10-07¶
The first release, as alpha: this is what is built.
Added¶
- One plan for a whole deploy.
lely.yml, or[tool.lely]inpyproject.toml, lists steps in the order they run; each is done by a plugin. A step takes what a step above it gives (${steps.<name>.<output>}) and values from the environment (${env.NAME}, always a secret).lely validatechecks all of it offline and prints the wiring. lely planplans every step and changes nothing. Each step is ready, waiting — it takes something that doesn't exist yet — or skipped, and the plan says which and why.--destroyplans a teardown, from the bottom up.lely applyandlely destroyrun a plan: a reviewed file (lely plan -o plan.json), or planned on the spot with-t. Nothing runs unasked — a terminal to answer at, or--yes— and a destructive change needs--allow-destructive. Every step is planned again right before it runs, and may do nothing the approved plan didn't show.- A plan file is held to what it was made for: the workspace, the steps as written, and the git tree of a clean checkout. It keeps no secret.
lely statuslists what exists because of each step, with links;lely showshows a saved plan;lely doctorsays which tools and which workspace lely finds.- Plugins.
bundledeploys a Databricks Asset Bundle with the Databricks CLI's direct engine;commandruns commands you give it, with an optional plan and destroy command;bundle.runruns a job or pipeline of a bundle step. A class in the repo (./ops/steps.py:Class) or in an installed package is a plugin too.lely stepslists them,lely schemawrites a JSON Schema of the config for editors, andlely.testingholds a plugin to the contract. - GitHub. With
--github, in a GitHub Actions run, the plan is one comment on the pull request, kept current, and the run's page says what was planned or done.-f mdprints the same Markdown.docs/GITHUB.mdhas workflows to copy. - A page.
lely ui plan.jsonmakes one HTML file of a plan — or of a run's record, written byapply -oanddestroy -o— with nothing to fetch and nothing that runs. A plugin can give its plan a view of its own; the bundle shows every resource by type.
Known limits¶
- Tried on a real workspace and on GitHub a few times, with small bundles: a first proof,
not a track record.
README.md, "What has been tried", says what was and wasn't. - The
stevinplugin plans and can't apply yet.