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.
- lely-0.1.0/.github/workflows/ci.yml +25 -0
- lely-0.1.0/.github/workflows/docs.yml +60 -0
- lely-0.1.0/.github/workflows/release.yml +131 -0
- lely-0.1.0/.gitignore +11 -0
- lely-0.1.0/.pre-commit-config.yaml +8 -0
- lely-0.1.0/CHANGELOG.md +47 -0
- lely-0.1.0/CLAUDE.md +105 -0
- lely-0.1.0/CONTRIBUTING.md +78 -0
- lely-0.1.0/LICENSE +202 -0
- lely-0.1.0/PKG-INFO +291 -0
- lely-0.1.0/README.md +261 -0
- lely-0.1.0/docs/DESIGN.md +680 -0
- lely-0.1.0/docs/GITHUB.md +340 -0
- lely-0.1.0/docs/assets/page-plan-dark.png +0 -0
- lely-0.1.0/docs/assets/page-plan-light.png +0 -0
- lely-0.1.0/docs/assets/page-run-dark.png +0 -0
- lely-0.1.0/docs/assets/page-run-light.png +0 -0
- lely-0.1.0/docs/changelog.md +1 -0
- lely-0.1.0/docs/cli.md +47 -0
- lely-0.1.0/docs/config.md +105 -0
- lely-0.1.0/docs/contributing.md +1 -0
- lely-0.1.0/docs/getting-started.md +90 -0
- lely-0.1.0/docs/index.md +80 -0
- lely-0.1.0/docs/installation.md +64 -0
- lely-0.1.0/docs/page.md +56 -0
- lely-0.1.0/docs/plan-apply-destroy.md +100 -0
- lely-0.1.0/docs/plugins.md +108 -0
- lely-0.1.0/docs/safety.md +59 -0
- lely-0.1.0/docs/stylesheets/extra.css +64 -0
- lely-0.1.0/docs/tried.md +54 -0
- lely-0.1.0/docs/writing-a-plugin.md +143 -0
- lely-0.1.0/mise.toml +53 -0
- lely-0.1.0/mkdocs.yml +101 -0
- lely-0.1.0/pyproject.toml +88 -0
- lely-0.1.0/spec/000-what-lely-is.md +189 -0
- lely-0.1.0/spec/001-what-is-built.md +108 -0
- lely-0.1.0/spec/002-plugins.md +291 -0
- lely-0.1.0/spec/003-config.md +127 -0
- lely-0.1.0/spec/004-asset-bundle.md +300 -0
- lely-0.1.0/spec/005-plan-apply-destroy.md +307 -0
- lely-0.1.0/spec/006-stevin.md +29 -0
- lely-0.1.0/spec/007-ui.md +129 -0
- lely-0.1.0/spec/008-github-actions.md +142 -0
- lely-0.1.0/spec/009-first-release.md +87 -0
- lely-0.1.0/spec/010-command-and-bundle-run.md +118 -0
- lely-0.1.0/spec/README.md +154 -0
- lely-0.1.0/src/lely/__init__.py +15 -0
- lely-0.1.0/src/lely/__main__.py +6 -0
- lely-0.1.0/src/lely/approval.py +206 -0
- lely-0.1.0/src/lely/cli.py +1315 -0
- lely-0.1.0/src/lely/config.py +582 -0
- lely-0.1.0/src/lely/databricks.py +64 -0
- lely-0.1.0/src/lely/errors.py +26 -0
- lely-0.1.0/src/lely/github.py +507 -0
- lely-0.1.0/src/lely/model.py +463 -0
- lely-0.1.0/src/lely/options.py +355 -0
- lely-0.1.0/src/lely/planfile.py +721 -0
- lely-0.1.0/src/lely/planning.py +663 -0
- lely-0.1.0/src/lely/process.py +56 -0
- lely-0.1.0/src/lely/py.typed +0 -0
- lely-0.1.0/src/lely/refs.py +370 -0
- lely-0.1.0/src/lely/registry.py +198 -0
- lely-0.1.0/src/lely/render/__init__.py +2 -0
- lely-0.1.0/src/lely/render/html.py +542 -0
- lely-0.1.0/src/lely/render/markdown.py +586 -0
- lely-0.1.0/src/lely/render/rich.py +323 -0
- lely-0.1.0/src/lely/render/words.py +285 -0
- lely-0.1.0/src/lely/running.py +420 -0
- lely-0.1.0/src/lely/schema.py +275 -0
- lely-0.1.0/src/lely/source.py +312 -0
- lely-0.1.0/src/lely/step.py +244 -0
- lely-0.1.0/src/lely/steps/__init__.py +1 -0
- lely-0.1.0/src/lely/steps/bundle.py +550 -0
- lely-0.1.0/src/lely/steps/bundle_run.py +99 -0
- lely-0.1.0/src/lely/steps/command.py +293 -0
- lely-0.1.0/src/lely/steps/stevin.py +152 -0
- lely-0.1.0/src/lely/testing.py +238 -0
- lely-0.1.0/tests/browser/test_frame_in_a_browser.py +357 -0
- lely-0.1.0/tests/fake_databricks.py +386 -0
- lely-0.1.0/tests/fake_github.py +162 -0
- lely-0.1.0/tests/fake_stevin.py +33 -0
- lely-0.1.0/tests/fakes.py +129 -0
- lely-0.1.0/tests/fixtures/cli/README.md +23 -0
- lely-0.1.0/tests/fixtures/cli/plan-create.json +49 -0
- lely-0.1.0/tests/fixtures/cli/plan-delete-update.json +107 -0
- lely-0.1.0/tests/fixtures/cli/plan-update-recreate.json +105 -0
- lely-0.1.0/tests/fixtures/cli/summary-default-python.json +137 -0
- lely-0.1.0/tests/fixtures/cli/validate-default-python.json +133 -0
- lely-0.1.0/tests/fixtures/stevin-create.json +385 -0
- lely-0.1.0/tests/fixtures/stevin-destroy.json +362 -0
- lely-0.1.0/tests/fixtures/stevin-empty.json +338 -0
- lely-0.1.0/tests/project.py +163 -0
- lely-0.1.0/tests/unit/__snapshots__/test_render_html.ambr +81 -0
- lely-0.1.0/tests/unit/__snapshots__/test_render_markdown.ambr +185 -0
- lely-0.1.0/tests/unit/__snapshots__/test_render_rich.ambr +183 -0
- lely-0.1.0/tests/unit/conftest.py +51 -0
- lely-0.1.0/tests/unit/test_approval.py +276 -0
- lely-0.1.0/tests/unit/test_cli.py +1770 -0
- lely-0.1.0/tests/unit/test_config.py +381 -0
- lely-0.1.0/tests/unit/test_databricks.py +74 -0
- lely-0.1.0/tests/unit/test_github.py +589 -0
- lely-0.1.0/tests/unit/test_options.py +380 -0
- lely-0.1.0/tests/unit/test_planfile.py +738 -0
- lely-0.1.0/tests/unit/test_planning.py +865 -0
- lely-0.1.0/tests/unit/test_process.py +25 -0
- lely-0.1.0/tests/unit/test_quietly.py +117 -0
- lely-0.1.0/tests/unit/test_refs.py +282 -0
- lely-0.1.0/tests/unit/test_registry.py +193 -0
- lely-0.1.0/tests/unit/test_render_html.py +496 -0
- lely-0.1.0/tests/unit/test_render_markdown.py +643 -0
- lely-0.1.0/tests/unit/test_render_rich.py +196 -0
- lely-0.1.0/tests/unit/test_repo.py +44 -0
- lely-0.1.0/tests/unit/test_running.py +968 -0
- lely-0.1.0/tests/unit/test_schema.py +425 -0
- lely-0.1.0/tests/unit/test_source.py +463 -0
- lely-0.1.0/tests/unit/test_step_bundle.py +648 -0
- lely-0.1.0/tests/unit/test_step_bundle_run.py +126 -0
- lely-0.1.0/tests/unit/test_step_command.py +405 -0
- lely-0.1.0/tests/unit/test_step_stevin.py +123 -0
- lely-0.1.0/tests/unit/test_testing.py +278 -0
- 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,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
|
lely-0.1.0/CHANGELOG.md
ADDED
|
@@ -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.
|