lely 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. lely-0.1.0/.github/workflows/ci.yml +25 -0
  2. lely-0.1.0/.github/workflows/docs.yml +60 -0
  3. lely-0.1.0/.github/workflows/release.yml +131 -0
  4. lely-0.1.0/.gitignore +11 -0
  5. lely-0.1.0/.pre-commit-config.yaml +8 -0
  6. lely-0.1.0/CHANGELOG.md +47 -0
  7. lely-0.1.0/CLAUDE.md +105 -0
  8. lely-0.1.0/CONTRIBUTING.md +78 -0
  9. lely-0.1.0/LICENSE +202 -0
  10. lely-0.1.0/PKG-INFO +291 -0
  11. lely-0.1.0/README.md +261 -0
  12. lely-0.1.0/docs/DESIGN.md +680 -0
  13. lely-0.1.0/docs/GITHUB.md +340 -0
  14. lely-0.1.0/docs/assets/page-plan-dark.png +0 -0
  15. lely-0.1.0/docs/assets/page-plan-light.png +0 -0
  16. lely-0.1.0/docs/assets/page-run-dark.png +0 -0
  17. lely-0.1.0/docs/assets/page-run-light.png +0 -0
  18. lely-0.1.0/docs/changelog.md +1 -0
  19. lely-0.1.0/docs/cli.md +47 -0
  20. lely-0.1.0/docs/config.md +105 -0
  21. lely-0.1.0/docs/contributing.md +1 -0
  22. lely-0.1.0/docs/getting-started.md +90 -0
  23. lely-0.1.0/docs/index.md +80 -0
  24. lely-0.1.0/docs/installation.md +64 -0
  25. lely-0.1.0/docs/page.md +56 -0
  26. lely-0.1.0/docs/plan-apply-destroy.md +100 -0
  27. lely-0.1.0/docs/plugins.md +108 -0
  28. lely-0.1.0/docs/safety.md +59 -0
  29. lely-0.1.0/docs/stylesheets/extra.css +64 -0
  30. lely-0.1.0/docs/tried.md +54 -0
  31. lely-0.1.0/docs/writing-a-plugin.md +143 -0
  32. lely-0.1.0/mise.toml +53 -0
  33. lely-0.1.0/mkdocs.yml +101 -0
  34. lely-0.1.0/pyproject.toml +88 -0
  35. lely-0.1.0/spec/000-what-lely-is.md +189 -0
  36. lely-0.1.0/spec/001-what-is-built.md +108 -0
  37. lely-0.1.0/spec/002-plugins.md +291 -0
  38. lely-0.1.0/spec/003-config.md +127 -0
  39. lely-0.1.0/spec/004-asset-bundle.md +300 -0
  40. lely-0.1.0/spec/005-plan-apply-destroy.md +307 -0
  41. lely-0.1.0/spec/006-stevin.md +29 -0
  42. lely-0.1.0/spec/007-ui.md +129 -0
  43. lely-0.1.0/spec/008-github-actions.md +142 -0
  44. lely-0.1.0/spec/009-first-release.md +87 -0
  45. lely-0.1.0/spec/010-command-and-bundle-run.md +118 -0
  46. lely-0.1.0/spec/README.md +154 -0
  47. lely-0.1.0/src/lely/__init__.py +15 -0
  48. lely-0.1.0/src/lely/__main__.py +6 -0
  49. lely-0.1.0/src/lely/approval.py +206 -0
  50. lely-0.1.0/src/lely/cli.py +1315 -0
  51. lely-0.1.0/src/lely/config.py +582 -0
  52. lely-0.1.0/src/lely/databricks.py +64 -0
  53. lely-0.1.0/src/lely/errors.py +26 -0
  54. lely-0.1.0/src/lely/github.py +507 -0
  55. lely-0.1.0/src/lely/model.py +463 -0
  56. lely-0.1.0/src/lely/options.py +355 -0
  57. lely-0.1.0/src/lely/planfile.py +721 -0
  58. lely-0.1.0/src/lely/planning.py +663 -0
  59. lely-0.1.0/src/lely/process.py +56 -0
  60. lely-0.1.0/src/lely/py.typed +0 -0
  61. lely-0.1.0/src/lely/refs.py +370 -0
  62. lely-0.1.0/src/lely/registry.py +198 -0
  63. lely-0.1.0/src/lely/render/__init__.py +2 -0
  64. lely-0.1.0/src/lely/render/html.py +542 -0
  65. lely-0.1.0/src/lely/render/markdown.py +586 -0
  66. lely-0.1.0/src/lely/render/rich.py +323 -0
  67. lely-0.1.0/src/lely/render/words.py +285 -0
  68. lely-0.1.0/src/lely/running.py +420 -0
  69. lely-0.1.0/src/lely/schema.py +275 -0
  70. lely-0.1.0/src/lely/source.py +312 -0
  71. lely-0.1.0/src/lely/step.py +244 -0
  72. lely-0.1.0/src/lely/steps/__init__.py +1 -0
  73. lely-0.1.0/src/lely/steps/bundle.py +550 -0
  74. lely-0.1.0/src/lely/steps/bundle_run.py +99 -0
  75. lely-0.1.0/src/lely/steps/command.py +293 -0
  76. lely-0.1.0/src/lely/steps/stevin.py +152 -0
  77. lely-0.1.0/src/lely/testing.py +238 -0
  78. lely-0.1.0/tests/browser/test_frame_in_a_browser.py +357 -0
  79. lely-0.1.0/tests/fake_databricks.py +386 -0
  80. lely-0.1.0/tests/fake_github.py +162 -0
  81. lely-0.1.0/tests/fake_stevin.py +33 -0
  82. lely-0.1.0/tests/fakes.py +129 -0
  83. lely-0.1.0/tests/fixtures/cli/README.md +23 -0
  84. lely-0.1.0/tests/fixtures/cli/plan-create.json +49 -0
  85. lely-0.1.0/tests/fixtures/cli/plan-delete-update.json +107 -0
  86. lely-0.1.0/tests/fixtures/cli/plan-update-recreate.json +105 -0
  87. lely-0.1.0/tests/fixtures/cli/summary-default-python.json +137 -0
  88. lely-0.1.0/tests/fixtures/cli/validate-default-python.json +133 -0
  89. lely-0.1.0/tests/fixtures/stevin-create.json +385 -0
  90. lely-0.1.0/tests/fixtures/stevin-destroy.json +362 -0
  91. lely-0.1.0/tests/fixtures/stevin-empty.json +338 -0
  92. lely-0.1.0/tests/project.py +163 -0
  93. lely-0.1.0/tests/unit/__snapshots__/test_render_html.ambr +81 -0
  94. lely-0.1.0/tests/unit/__snapshots__/test_render_markdown.ambr +185 -0
  95. lely-0.1.0/tests/unit/__snapshots__/test_render_rich.ambr +183 -0
  96. lely-0.1.0/tests/unit/conftest.py +51 -0
  97. lely-0.1.0/tests/unit/test_approval.py +276 -0
  98. lely-0.1.0/tests/unit/test_cli.py +1770 -0
  99. lely-0.1.0/tests/unit/test_config.py +381 -0
  100. lely-0.1.0/tests/unit/test_databricks.py +74 -0
  101. lely-0.1.0/tests/unit/test_github.py +589 -0
  102. lely-0.1.0/tests/unit/test_options.py +380 -0
  103. lely-0.1.0/tests/unit/test_planfile.py +738 -0
  104. lely-0.1.0/tests/unit/test_planning.py +865 -0
  105. lely-0.1.0/tests/unit/test_process.py +25 -0
  106. lely-0.1.0/tests/unit/test_quietly.py +117 -0
  107. lely-0.1.0/tests/unit/test_refs.py +282 -0
  108. lely-0.1.0/tests/unit/test_registry.py +193 -0
  109. lely-0.1.0/tests/unit/test_render_html.py +496 -0
  110. lely-0.1.0/tests/unit/test_render_markdown.py +643 -0
  111. lely-0.1.0/tests/unit/test_render_rich.py +196 -0
  112. lely-0.1.0/tests/unit/test_repo.py +44 -0
  113. lely-0.1.0/tests/unit/test_running.py +968 -0
  114. lely-0.1.0/tests/unit/test_schema.py +425 -0
  115. lely-0.1.0/tests/unit/test_source.py +463 -0
  116. lely-0.1.0/tests/unit/test_step_bundle.py +648 -0
  117. lely-0.1.0/tests/unit/test_step_bundle_run.py +126 -0
  118. lely-0.1.0/tests/unit/test_step_command.py +405 -0
  119. lely-0.1.0/tests/unit/test_step_stevin.py +123 -0
  120. lely-0.1.0/tests/unit/test_testing.py +278 -0
  121. lely-0.1.0/uv.lock +1284 -0
@@ -0,0 +1,25 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ # ruff and the tests on every supported Python — the same gate for every
10
+ # Kostavo tool, kept in one place.
11
+ python:
12
+ uses: kostavo-oss/.github/.github/workflows/python-ci.yml@main
13
+
14
+ # `mise run check` also runs ty, which the shared gate doesn't: so here it is,
15
+ # and the gate in CI is the gate at a desk.
16
+ types:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v7
20
+ with:
21
+ persist-credentials: false
22
+ - uses: astral-sh/setup-uv@v7
23
+ - run: uv sync
24
+ - name: Type check (ty)
25
+ run: uv run ty check
@@ -0,0 +1,60 @@
1
+ name: docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - "docs/**"
8
+ - "mkdocs.yml"
9
+ - "CONTRIBUTING.md"
10
+ - "CHANGELOG.md"
11
+ - ".github/workflows/docs.yml"
12
+ pull_request:
13
+ paths:
14
+ - "docs/**"
15
+ - "mkdocs.yml"
16
+ - "CONTRIBUTING.md"
17
+ - "CHANGELOG.md"
18
+ - ".github/workflows/docs.yml"
19
+ workflow_dispatch:
20
+
21
+ # allow the GITHUB_TOKEN to deploy to Pages, and serialise deploys
22
+ permissions:
23
+ contents: read
24
+ pages: write
25
+ id-token: write
26
+
27
+ concurrency:
28
+ group: pages
29
+ cancel-in-progress: false
30
+
31
+ jobs:
32
+ build:
33
+ name: build site
34
+ runs-on: ubuntu-latest
35
+ env:
36
+ NO_MKDOCS_2_WARNING: "1" # we pin mkdocs < 2; suppress the advisory banner
37
+ steps:
38
+ - uses: actions/checkout@v7
39
+ with:
40
+ persist-credentials: false
41
+ - uses: astral-sh/setup-uv@v7
42
+ with:
43
+ enable-cache: true
44
+ - name: Build (mkdocs)
45
+ run: uv run --group docs mkdocs build --strict
46
+ - uses: actions/upload-pages-artifact@v5
47
+ with:
48
+ path: site
49
+
50
+ deploy:
51
+ name: deploy to Pages
52
+ needs: build
53
+ if: github.event_name != 'pull_request' # build-only on PRs; deploy from main
54
+ runs-on: ubuntu-latest
55
+ environment:
56
+ name: github-pages
57
+ url: ${{ steps.deployment.outputs.page_url }}
58
+ steps:
59
+ - id: deployment
60
+ uses: actions/deploy-pages@v5
@@ -0,0 +1,131 @@
1
+ name: release
2
+
3
+ # A release is a version. Bump `version` in pyproject.toml and move the
4
+ # CHANGELOG's [Unreleased] notes under it, in a pull request — merging that pull
5
+ # request is the release. The same way stevin and caland are released.
6
+ #
7
+ # This workflow notices a version on main that isn't out yet, runs the gate on
8
+ # it, publishes to PyPI with Trusted Publishing (repository kostavo-oss/lely,
9
+ # workflow release.yml, environment pypi — no token), and creates the GitHub
10
+ # release, which is what makes the `vX.Y.Z` tag.
11
+ #
12
+ # A version that is already tagged, or already on PyPI, is not a release: the
13
+ # run says so and stops. So is a push that leaves the version as it was. A
14
+ # pyproject.toml change that isn't a bump is free.
15
+
16
+ on:
17
+ push:
18
+ branches: [main]
19
+ paths: ["pyproject.toml"]
20
+ workflow_dispatch: # to pick a release up again after a failure
21
+
22
+ concurrency:
23
+ group: release
24
+ cancel-in-progress: false
25
+
26
+ permissions:
27
+ contents: read
28
+
29
+ jobs:
30
+ check:
31
+ name: is this a new version?
32
+ runs-on: ubuntu-latest
33
+ outputs:
34
+ version: ${{ steps.read.outputs.version }}
35
+ release: ${{ steps.read.outputs.release }}
36
+ steps:
37
+ - uses: actions/checkout@v7
38
+ with:
39
+ fetch-depth: 0 # the tags, to tell what is already out
40
+ persist-credentials: false
41
+ - id: read
42
+ name: The version, and whether it is already released
43
+ env:
44
+ EVENT: ${{ github.event_name }}
45
+ run: |
46
+ version=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
47
+ echo "version=$version" >> "$GITHUB_OUTPUT"
48
+ if git rev-parse -q --verify "refs/tags/v$version" >/dev/null; then
49
+ echo "v$version is already tagged — nothing to release."
50
+ echo "release=false" >> "$GITHUB_OUTPUT"
51
+ exit 0
52
+ fi
53
+ if curl -fsS -o /dev/null "https://pypi.org/pypi/lely/$version/json"; then
54
+ echo "lely $version is already on PyPI — nothing to release."
55
+ echo "release=false" >> "$GITHUB_OUTPUT"
56
+ exit 0
57
+ fi
58
+ # Before the first release no version is tagged, so "not out yet" is true
59
+ # of every push. A push that leaves the version as it was is not a bump.
60
+ # (Started by hand, this is skipped: that is how a release is picked up
61
+ # again after a failure.)
62
+ if [ "$EVENT" = push ]; then
63
+ before=$(git show "HEAD^:pyproject.toml" 2>/dev/null | python3 -c "import sys, tomllib; print(tomllib.loads(sys.stdin.read())['project']['version'])" 2>/dev/null || true)
64
+ if [ "$before" = "$version" ]; then
65
+ echo "The version is $version, as it was — nothing to release."
66
+ echo "release=false" >> "$GITHUB_OUTPUT"
67
+ exit 0
68
+ fi
69
+ fi
70
+ if ! grep -q "^## \[$version\]" CHANGELOG.md; then
71
+ echo "::error::CHANGELOG.md says nothing about $version — move the [Unreleased] notes under a '## [$version] - <date>' heading"
72
+ exit 1
73
+ fi
74
+ echo "releasing $version"
75
+ echo "release=true" >> "$GITHUB_OUTPUT"
76
+
77
+ gate:
78
+ name: gate
79
+ needs: check
80
+ if: needs.check.outputs.release == 'true'
81
+ runs-on: ubuntu-latest
82
+ steps:
83
+ - uses: actions/checkout@v7
84
+ with:
85
+ persist-credentials: false
86
+ - uses: astral-sh/setup-uv@v7
87
+ - run: uv sync
88
+ - run: uv run ruff check .
89
+ - run: uv run ruff format --check .
90
+ - run: uv run ty check
91
+ - run: uv run pytest
92
+
93
+ release:
94
+ name: publish v${{ needs.check.outputs.version }}
95
+ needs: [check, gate]
96
+ if: needs.check.outputs.release == 'true'
97
+ runs-on: ubuntu-latest
98
+ environment: pypi
99
+ permissions:
100
+ contents: write # the GitHub release and its tag
101
+ id-token: write # PyPI Trusted Publishing (OIDC)
102
+ steps:
103
+ - uses: actions/checkout@v7
104
+ with:
105
+ persist-credentials: false
106
+ - uses: astral-sh/setup-uv@v7
107
+ - name: Build
108
+ run: uv build
109
+ - name: Publish to PyPI
110
+ # First, because it is the step that can't be undone: a tag without a
111
+ # release can be pushed again, a release without a tag can't.
112
+ run: uv publish --trusted-publishing always --check-url https://pypi.org/simple/
113
+ - name: Release notes from CHANGELOG
114
+ env:
115
+ VERSION: ${{ needs.check.outputs.version }}
116
+ run: |
117
+ awk -v v="$VERSION" '
118
+ index($0, "## [" v "]") == 1 { f=1; next }
119
+ f && index($0, "## [") == 1 { exit }
120
+ f { print }
121
+ ' CHANGELOG.md > notes.md || true
122
+ - name: Create the GitHub release, which makes the tag
123
+ env:
124
+ GH_TOKEN: ${{ github.token }}
125
+ VERSION: ${{ needs.check.outputs.version }}
126
+ run: |
127
+ args=(--title "v$VERSION" --target "$GITHUB_SHA")
128
+ if [ -s notes.md ]; then args+=(--notes-file notes.md); else args+=(--generate-notes); fi
129
+ # 0.1.0a1, 0.2.0rc1 … are PEP 440 pre-releases: say so on GitHub too.
130
+ if [[ "$VERSION" =~ (a|b|rc)[0-9]+$ ]]; then args+=(--prerelease); fi
131
+ gh release create "v$VERSION" dist/* "${args[@]}"
lely-0.1.0/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .DS_Store
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ plan.json
11
+ site/
@@ -0,0 +1,8 @@
1
+ # `uvx pre-commit install` once, and ruff runs on every commit.
2
+ repos:
3
+ - repo: https://github.com/astral-sh/ruff-pre-commit
4
+ rev: v0.16.9 # the ruff uv.lock has, so a commit is checked as ci checks it
5
+ hooks:
6
+ - id: ruff-check
7
+ args: [--fix]
8
+ - id: ruff-format
@@ -0,0 +1,47 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-10-07
10
+
11
+ The first release, as alpha: this is what is built.
12
+
13
+ ### Added
14
+
15
+ - **One plan for a whole deploy.** `lely.yml`, or `[tool.lely]` in `pyproject.toml`, lists
16
+ steps in the order they run; each is done by a plugin. A step takes what a step above it
17
+ gives (`${steps.<name>.<output>}`) and values from the environment (`${env.NAME}`, always a
18
+ secret). `lely validate` checks all of it offline and prints the wiring.
19
+ - **`lely plan`** plans every step and changes nothing. Each step is ready, waiting — it
20
+ takes something that doesn't exist yet — or skipped, and the plan says which and why.
21
+ `--destroy` plans a teardown, from the bottom up.
22
+ - **`lely apply` and `lely destroy`** run a plan: a reviewed file (`lely plan -o plan.json`),
23
+ or planned on the spot with `-t`. Nothing runs unasked — a terminal to answer at, or
24
+ `--yes` — and a destructive change needs `--allow-destructive`. Every step is planned
25
+ again right before it runs, and may do nothing the approved plan didn't show.
26
+ - **A plan file is held to what it was made for**: the workspace, the steps as written, and
27
+ the git tree of a clean checkout. It keeps no secret.
28
+ - **`lely status`** lists what exists because of each step, with links; `lely show` shows a
29
+ saved plan; `lely doctor` says which tools and which workspace lely finds.
30
+ - **Plugins.** `bundle` deploys a Databricks Asset Bundle with the Databricks CLI's direct
31
+ engine; `command` runs commands you give it, with an optional plan and destroy command;
32
+ `bundle.run` runs a job or pipeline of a bundle step. A class in the repo
33
+ (`./ops/steps.py:Class`) or in an installed package is a plugin too. `lely steps` lists
34
+ them, `lely schema` writes a JSON Schema of the config for editors, and `lely.testing`
35
+ holds a plugin to the contract.
36
+ - **GitHub.** With `--github`, in a GitHub Actions run, the plan is one comment on the pull
37
+ request, kept current, and the run's page says what was planned or done. `-f md` prints
38
+ the same Markdown. `docs/GITHUB.md` has workflows to copy.
39
+ - **A page.** `lely ui plan.json` makes one HTML file of a plan — or of a run's record,
40
+ written by `apply -o` and `destroy -o` — with nothing to fetch and nothing that runs. A
41
+ plugin can give its plan a view of its own; the bundle shows every resource by type.
42
+
43
+ ### Known limits
44
+
45
+ - Tried on a real workspace and on GitHub a few times, with small bundles: a first proof,
46
+ not a track record. `README.md`, "What has been tried", says what was and wasn't.
47
+ - The `stevin` plugin plans and can't apply yet.
lely-0.1.0/CLAUDE.md ADDED
@@ -0,0 +1,105 @@
1
+ # CLAUDE.md
2
+
3
+ `lely`: one plan for a whole Databricks deploy. It plans, applies and destroys an ordered list
4
+ of steps, each done by a plugin, and keeps no state. The Asset Bundle is one of those plugins.
5
+
6
+ `spec/` says *what* each piece of work must deliver and when it is done; `docs/DESIGN.md` says
7
+ *how* it is built. Read the spec of a piece before working on it. Since 2026-10-05 the two
8
+ agree: if they disagree, or the code disagrees with either, flag it instead of silently picking
9
+ one. Don't build past a "To decide" that is still open — those are the owner's to answer.
10
+
11
+ ## Rules
12
+
13
+ - The core is pure: config, references, the wiring, plan assembly, the approval check and the
14
+ renderers do no I/O, no SDK calls, no clock, no env. I/O lives at the edges: the plugins, the
15
+ Databricks CLI runner, the workspace client, `git`, the command line.
16
+ - No module outside `src/lely/steps/` knows a plugin by name — the bundle included.
17
+ - Domain model: frozen, slotted stdlib dataclasses with tuples.
18
+ - The bundle is the Databricks CLI's: the `bundle` plugin asks it
19
+ (`bundle summary/plan -o json`, `deploy --plan`, `destroy`) and never reimplements what it
20
+ resolves. Not `bundle validate`: on a real workspace it creates a folder, and a plan changes
21
+ nothing. If a bundle resource can manage something, no plugin does.
22
+ - A step is given its own options and a way to reach the workspace, nothing else. What it takes
23
+ from another step is `${steps.<name>.<output>}` in its `with:`, from a step above it.
24
+ - A plugin declares its outputs, touches only what its options name, and destroys only what it
25
+ can show is its own.
26
+ - No destructive change without `destructive`, and none applied without `--allow-destructive`.
27
+ - Nothing that changes a workspace runs unasked. A refusal is a `Refused` (exit 2); a failure
28
+ is any other `LelyError` (exit 1).
29
+ - Plan files carry no secrets. A value from the environment is a `Secret`.
30
+ - Nothing a plan says is Markdown or markup where it is shown: in a terminal its control
31
+ characters are made visible, on GitHub it stands in a block or a code span, on the page it
32
+ is escaped — and a plugin's view is written again from a short list of elements.
33
+ - Showing a plan runs none of the project's code: `show` and `ui` load no plugin.
34
+ - No raw invisible character in any file: write its escape. A test holds the repository to it.
35
+ - A file lely writes is written beside its place and moved there (`cli._write`): never
36
+ through a link, never half. What is no file — a device, a pipe — is written to as it is.
37
+ - With `--github` lely knows the run's token, and no line it says holds it (`cli._hide`); a
38
+ plan that holds it is shown by no command (`cli._holds_no_token`). A new place that prints
39
+ goes through one of the two.
40
+ - Every Databricks behaviour assumption gets a test and a link to the docs in its
41
+ docstring. If unsure, say so and add a `TODO(verify)` — do not guess.
42
+ - Small PR-sized commits, conventional commit messages.
43
+ - **A change to `version` in `pyproject.toml`, merged to `main`, publishes to PyPI**
44
+ (`.github/workflows/release.yml`). Never bump it in passing: a release is a pull request of
45
+ its own, opened when the owner says.
46
+
47
+ ## Docs
48
+
49
+ `docs/` is the site (MkDocs Material; `mkdocs.yml`), deployed from `main` to
50
+ <https://kostavo-oss.github.io/lely/>. `mise run docs:build` builds it in strict mode: a
51
+ broken link fails. A link out of `docs/` goes to the repository by its full address — the
52
+ site has only what is in `docs/`. The pictures of the page in `docs/assets/` are made from
53
+ the shared scenario; remake them when the page changes how it looks.
54
+
55
+ ## Commands
56
+
57
+ Tooling is mise + the Astral stack (uv, ruff, ty) — same as `stevin` and `caland`.
58
+ Never use pip/virtualenv, black/flake8/isort, or mypy.
59
+
60
+ `mise run check` is the gate (lint, format check, types, unit tests). `ruff format` also
61
+ formats the Python in Markdown code blocks, so the gate covers `docs/` and `spec/`. When
62
+ chaining the gate in a shell, a pipe hides a failing test: check the test count, not the exit
63
+ code of `tail`.
64
+
65
+ ## Status
66
+
67
+ **Phases one and two are built, and lely was run on a real workspace twice (2026-10-06).** `validate`, `steps`, `schema`, `plan`,
68
+ `show`, `apply`, `destroy`, `status` and `doctor`; the `bundle`, `command` and `bundle.run`
69
+ plugins and plugins from a repo file; the config in `lely.yml` or `pyproject.toml`.
70
+
71
+ Not built, or not proven:
72
+
73
+ - **Two runs on a real workspace are a first proof, not a track record.** What it settled, what
74
+ it corrected (the CLI goes to the bundle's host with the credentials at hand; `bundle
75
+ validate` writes, so the plugin uses `bundle summary`) and what it couldn't show is in
76
+ `spec/004-asset-bundle.md`, "Run on a workspace". What is still assumed is a `TODO(verify)`
77
+ in `src/lely/steps/bundle.py`.
78
+ - **`stevin` is parked** (`spec/006-stevin.md`): its plan half works; `apply` refuses a project
79
+ that uses it. The owner takes it up separately — don't extend it.
80
+ - **GitHub is built (2026-10-06) and has run once for real**: `--github` and `-f md`
81
+ (`spec/008-github-actions.md`, `docs/GITHUB.md`), tested against `tests/fake_github.py`,
82
+ and run through all three workflows in a private repository (`kostavo-oss/lely-proof`).
83
+ Not tried there: OIDC sign-in, environments, a fork — see `docs/GITHUB.md`.
84
+ - **The page is built (2026-10-06)**: `lely ui`, `StepPlan.view`, a run's record with
85
+ `apply -o` (`spec/007-ui.md`). Looked at in a browser and reviewed once. Whether a plugin's
86
+ view stays in its frame is asked of a real browser: `uv run pytest tests/browser`. It needs
87
+ Chrome and skips without it; `mise run check` doesn't run it, CI does (its runners have
88
+ Chrome). Run it after any change to `render/html.py`.
89
+
90
+ There is no Databricks CLI or workspace in the unit suite:
91
+
92
+ - `tests/fake_databricks.py` is the CLI as a fake, as a program and in process. It answers from
93
+ recordings, or simulates a bundle in a workspace kept in a folder: `plan`, `deploy`,
94
+ `summary`, `destroy`, `run`. What it simulates is what lely *believes* the CLI does.
95
+ - `tests/fixtures/cli/` holds the CLI's own recorded outputs, from its acceptance tests.
96
+ - `tests/fake_github.py` is a pull request's comments in memory, and a run's environment. What
97
+ it simulates is what lely *believes* GitHub does.
98
+ - `tests/fixtures/stevin-*.json` are real stevin plan files, written by stevin against its fake
99
+ warehouse; `tests/fake_stevin.py` answers from them.
100
+ - `tests/project.py` is the scenario most tests share.
101
+
102
+ **lely was sluis** until 2026-10-05, when the suite took its names from Dutch engineers
103
+ (stevin, lely, and caland — which was maeslant for a day, until the owner wanted a person's
104
+ name there too). It had never been published, so nothing answers to the old name. The recorded plans in `tests/fixtures/stevin-*.json` still carry `deltaplan.managed` —
105
+ that is the property stevin really writes onto tables, and it kept its name on purpose.
@@ -0,0 +1,78 @@
1
+ # Contributing to lely
2
+
3
+ Issues and pull requests are welcome.
4
+
5
+ `lely` is alpha: everything in the specs is built, and it has run for real only a few
6
+ times. `spec/` says what each piece must deliver, `docs/DESIGN.md` how it is built. If the
7
+ code disagrees with either, that is a bug in one of them — please say which.
8
+
9
+ ## How a change lands
10
+
11
+ Every change is a pull request against `main`, merged by a maintainer once its checks pass.
12
+
13
+ - **`ci`** — lint, format, types, and the tests on Python 3.11–3.13. Required.
14
+ - Small commits with [conventional](https://www.conventionalcommits.org) messages. New
15
+ behaviour comes with a test; a fix comes with the test that would have caught it.
16
+ - A change to what lely shows, asks or refuses updates the spec's "As built" and, where a
17
+ user would notice, `CHANGELOG.md`.
18
+
19
+ Some parts decide what may run, or handle what isn't trusted: `approval.py`, `running.py`,
20
+ `source.py`, `planfile.py`, `cli.py`, `github.py`, and the renderers. **A change there is
21
+ reviewed by someone who didn't write it before it is opened as a pull request** — in this
22
+ project's short history, most defects in those files came in with the fix for another.
23
+
24
+ ## Toolchain
25
+
26
+ [`mise`](https://mise.jdx.dev) pins the tools, and the stack is Astral's:
27
+ [`uv`](https://docs.astral.sh/uv/), [`ruff`](https://docs.astral.sh/ruff/) and
28
+ [`ty`](https://docs.astral.sh/ty/).
29
+
30
+ ```sh
31
+ mise install # the pinned Python and uv (optional but recommended)
32
+ uv sync # .venv with dependencies and dev tools
33
+ mise run check # the gate: lint, format check, types, unit tests
34
+ ```
35
+
36
+ ## Day to day
37
+
38
+ ```sh
39
+ uv run lely # the CLI
40
+ uv run pytest tests/unit # the unit tests: no workspace, no network
41
+ uv run pytest tests/browser # a plugin's view, as Chrome reads it (needs Chrome)
42
+ uv run ruff check . && uv run ruff format .
43
+ uv run ty check
44
+ mise run docs # the docs site, at localhost:8000
45
+ ```
46
+
47
+ `ruff format` also formats the Python in Markdown code blocks, so the gate covers `docs/` and
48
+ `spec/`. When you chain the gate in a shell, a pipe hides a failing test: read the test
49
+ count, not the exit code of `tail`.
50
+
51
+ The unit suite never reaches a workspace or GitHub: `tests/fake_databricks.py` and
52
+ `tests/fake_github.py` stand in, and simulate what lely *believes* those do. What has been
53
+ seen on the real things is written down in `spec/004-asset-bundle.md` and `docs/GITHUB.md`;
54
+ a new assumption about either gets a test, a link to the docs it rests on, and — until it has
55
+ been seen — a `TODO(verify)`.
56
+
57
+ ## Rules the code keeps
58
+
59
+ `CLAUDE.md` lists them. The ones a contributor trips over first:
60
+
61
+ - The core is pure. I/O lives at the edges: the plugins, the Databricks CLI runner, `git`,
62
+ GitHub, the command line.
63
+ - No module outside `src/lely/steps/` knows a plugin by name — the bundle included.
64
+ - Nothing that changes a workspace runs unasked, and nothing destructive without
65
+ `--allow-destructive`.
66
+ - Nothing a plan says is obeyed where it is shown — a terminal, Markdown, the page.
67
+ - A plan file carries no secret.
68
+
69
+ ## Releases
70
+
71
+ A release is a version, the same way as for stevin and caland: a pull request bumps
72
+ `version` in `pyproject.toml` and moves the changelog's *Unreleased* notes under a
73
+ `## [X.Y.Z] - <date>` heading, and merging it is the release. `.github/workflows/release.yml`
74
+ notices the new version on `main`, runs the gate, publishes to PyPI with Trusted Publishing —
75
+ no token — and makes the GitHub release and its tag. A change to `pyproject.toml` that isn't a
76
+ new version releases nothing.
77
+
78
+ The first release was 0.1.0, on 2026-10-07.