Skip to content

Commit 9fb69b6

Browse files
timzsuclaude
andauthored
refactor: split packages and add release machinery (#5)
* refactor: split workspace, publish server image to GHCR Rework the OSS release machinery to mirror FlowMesh's pattern: the ``lumilake`` distribution becomes a thin metapackage with extras ``[sdk]``/``[cli]``/``[deploy]``/``[hook]``/``[all]`` that pull in four workspace wheels; the server runtime lives in ``src/lumilake_server/`` and ships only inside the Docker image, never on PyPI. Workspace: - packages/sdk publishes the ``lumilake`` Python module (Client, envs, log). Other workspace packages depend on it as the shared base. - packages/cli publishes ``lumilake_cli`` (Typer entry point ``lumilake``). - packages/deploy publishes ``lumilake_deploy`` (Docker / FlowMesh glue). - packages/hook publishes ``lumilake_hook`` (lumid-hooks bindings). - Server code moved from ``src/lumilake/*`` to ``src/lumilake_server/*``; it stays out of every wheel. Image release: - New ``.github/workflows/image-publish.yml`` builds linux/amd64+arm64 via buildx and pushes to ``ghcr.io/mlsys-io/lumilake_server`` with tags ``:<sha>`` per push, ``:dev`` on main, and ``:<semver>`` + ``:latest`` on tag-v*. GHCR auth uses the workflow's ``GITHUB_TOKEN``. - ``scripts/dev/sync_requirements.py`` derives ``src/lumilake_server/requirements.txt`` from the ``server-runtime`` uv dependency group so the Dockerfile's pin set tracks ``uv.lock``. - Dockerfile rewritten to install the published ``lumilake-sdk`` wheel plus the generated requirements, copy ``src/lumilake_server/`` onto ``PYTHONPATH``, and run ``python -m lumilake_server.main``. - docker-compose ``server`` service now pulls ``\${LUMILAKE_REGISTRY:-ghcr.io/mlsys-io}/lumilake_server:\${LUMILAKE_IMAGE_TAG:-latest}`` (no ``build:`` stanza). CLI: - ``LUMILAKE_REGISTRY`` env var (default ``ghcr.io/mlsys-io``) selects the image registry. - New ``lumilake deploy pull`` command pulls the published image. - ``lumilake deploy up`` no longer auto-builds — it fails fast if the image is missing, pointing the user at ``pull`` or ``build``. - ``lumilake deploy build`` retains source builds (re-tagged for GHCR). PyPI publish: - ``.github/workflows/package-build.yml`` now builds all workspace packages (``uv build --all-packages``) and, on a tag-v*, uploads the dist artifact and publishes via OIDC trusted publishing (``pypa/gh-action-pypi-publish``). - ``scripts/ci/check_package_build.py`` validates the metapackage is code-free, each workspace wheel ships its top-level module, none of them contain ``lumilake_server/``, and smoke-installs every extra. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu> * refactor: harden release machinery and packaged deploy assets Post-review tightening of the GHCR + PyPI release flow introduced in 5d5ed47. Adopts FlowMesh's release-day machinery and moves the deploy templates into ``lumilake-deploy`` package data so PyPI users can run ``lumilake deploy`` without a source checkout. Release machinery (FlowMesh-aligned): - ``docs/RELEASE.md`` runbook covers PyPI Trusted Publisher pending- publisher setup, GHCR visibility + repo-link manual steps, ``pypi`` / ``testpypi`` / ``ghcr`` environment approvals, and the rollback recipes (yank, ``.postN``, "PyPI succeeded but image broken"). - ``scripts/ci/check_release_version.py`` cross-checks every ``version = ...`` string across the 5 pyprojects + 2 ``__version__`` literals against the release tag. - ``scripts/dev/bump_version.py`` writes them in one pass; release.yml re-runs ``--check`` after the version validator for belt-and-suspenders. - ``scripts/ci/check_image_release.py`` verifies the published image's per-platform OCI labels match the release tag + commit, polls for the manifest (race-safe against image-publish.yml), and is the gate for the ``:latest`` retag. - ``scripts/ci/retag_image_release.py`` writes ``:latest`` via ``docker buildx imagetools create``, with downgrade and missing-label-protection guards. - ``scripts/ci/filter_distributions.py`` trims dist files for staggered Trusted Publisher onboarding. - ``scripts/ci/append_release_digests.py`` sentinel-replace edits the Release body with the digest table, idempotent across re-runs. - New ``.github/workflows/release.yml`` (PyPI publish; GitHub release event, ``pypi`` environment approval) + ``release-images.yml`` (``:latest`` retag; ``ghcr`` environment approval). Both run on ``self-hosted`` to match FlowMesh. - ``image-publish.yml`` keeps pushing ``:sha`` / ``:dev`` / ``:vX.Y.Z`` per commit/tag but no longer pushes ``:latest`` (release-images owns it). Adds a post-push native-arch smoke (``import lumilake, lumilake_hook, lumilake_server``). - ``package-build.yml`` reduced to PR/main build + verify + ``twine check``; PyPI publish moved entirely to ``release.yml``. Packaged deploy assets: - ``.env.example`` and ``docker-compose.yml`` moved into ``packages/deploy/src/lumilake_deploy/assets/`` (compose renamed ``compose.yml``); root ``.env.example`` is now a symlink for editor / IDE convenience, matching FlowMesh. - New ``lumilake_deploy.assets`` module exposes ``asset_path`` / ``env_example_path`` / ``compose_path`` via ``importlib.resources`` so the deploy CLI works from any directory after a ``pip install``. - ``deploy init`` reads the packaged template; ``setup.py`` / ``stop.py`` run ``docker compose -f <compose_path()> --project-directory <deploy_dir>``. ``build_server_image`` now raises a friendly error when invoked without a source checkout's Dockerfile. ``-C`` / ``--project-dir`` (+ ``LUMILAKE_DEPLOY_DIR``) for the deploy CLI: every subcommand of ``lumilake deploy`` reads the env / compose project dir from the new option / env var, defaulting to cwd. Removes the source-checkout-required walker. CLI weight split: ``lumilake-cli`` drops ``lumilake-deploy`` from hard deps, exposes it as ``[deploy]`` extra. ``lumilake[cli]`` keeps the full lifecycle. ``commands/__init__.py`` uses ``find_spec`` to gate the deploy subcommands (real ImportError inside .deploy bubbles up loudly). Thin-install users hit an install hint for every deploy subcommand instead of typer's bare "No such command". SDK polish: - ``packages/sdk/src/lumilake/py.typed`` marker added. - ``resources/deploy.py`` switches the backend gate to ``find_spec``, reads ``ENV_FILE_NAME`` from the packaged constants, and resolves FlowMesh container names from ``.env.flowmesh`` via the shared ``lumilake_deploy.containers`` helper (was hardcoded ``flowmesh_node_*``). Exports ``SERVICE_NAMES`` (the slug-prefixed container names are per-deploy and resolved at call time). stop.py ``--wipe-archive`` derives the FlowMesh state-volume names from the same slug helper so an operator who ran ``init --flowmesh`` with the default ``lumilake`` suffix actually has their FlowMesh volumes removed. Per-package ``LICENSE`` files committed (workspace wheels were previously shipping without LICENSE due to a parent-traversal ``license-files``). ``check_package_build.py`` SHA-verifies every wheel's bundled LICENSE matches the repo root; it also catches flat-file leaks and enforces ``lumilake_server`` absent from every wheel. Server runtime / Dockerfile: non-root ``lumilake`` user (uid 10001), ``tini`` PID-1 + exec-form ``CMD``, ``COPY LICENSE ./LICENSE``, installs ``lumilake-sdk`` + ``lumilake-hook`` in-tree + flat ``requirements.txt`` generated by ``scripts/dev/sync_requirements.py`` (env-marker safe, fails loudly on unparsable specs). CI consistency: - All 9 ``setup-uv`` invocations now pin both ``version: "0.11.8"`` and ``python-version: "3.12"``. - ``env-examples.yml`` adds ``--extra deploy`` so the script can ``import lumilake_deploy.doctor``. - ``lint-typecheck.yml`` and ``unit-tests.yml`` add ``--frozen``. - ``security.yml`` bandit scans every package source root + ``scripts/``; pip-audit splits into image-runtime (server ``requirements.txt``) and wheel-install closure (``uv export --all-extras --no-default-groups --no-emit-workspace``). Docs: - ``README.md`` Quick Start + Deployment, ``docs/CLI.md``, ``docs/ENV.md``, ``CONTRIBUTING.md`` consistently use ``lumilake deploy -C <path>`` (or ``LUMILAKE_DEPLOY_DIR``), with explicit ``uv run`` vs PyPI install guidance. - ``docs/ARCHITECTURE.md``, ``docs/OPS.md``, ``docs/PLUGINS.md``, ``docs/CODE_STYLE.md``, ``docs/SDK.md`` updated for the workspace split (no more ``lumilake.{server,runtime,ops,utils,...}`` paths). - PR template + repository-layout block updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu> * ci: clear zizmor findings on image-publish.yml Pedantic-persona zizmor (the form CI runs under ``--persona pedantic --format github``) was failing with two findings on the image-publish workflow: * ``cache-poisoning`` (high) — ``astral-sh/setup-uv`` enables its cache by default. In a workflow that pushes signed artifacts to GHCR, a poisoned cache could influence the publishing step. Set ``enable-cache: false`` on this workflow's setup-uv invocation; the other setup-uv calls (lint, tests, env-examples, security, etc.) aren't publishing workflows and keep caching for speed. * ``undocumented-permissions`` (low) — ``packages: write`` at job level had no explanatory comment. Added the same style of inline comment release-images.yml already uses (`docker/login-action + build-push-action push to GHCR`). ``zizmor --persona pedantic --format github`` exits 0 after the fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu> * patch image publish workflow Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu> * docs: clarify image publish workflow Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu> --------- Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1c8db0d commit 9fb69b6

219 files changed

Lines changed: 4355 additions & 1069 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 0 additions & 78 deletions
This file was deleted.

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
packages/deploy/src/lumilake_deploy/assets/.env.example

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ Scope (optional): deploy, server, cli, runtime, docker, ...
1919

2020
<!-- List modified files or groups of files with a brief explanation. -->
2121
<!--
22-
- `src/lumilake/cli/commands/deploy.py` — added `--since` flag to `deploy logs`
23-
- `src/lumilake/deploy/docker_client.py` — thread `since` through to docker-py
22+
- `packages/cli/src/lumilake_cli/commands/deploy.py` — added `--since` flag to `deploy logs`
23+
- `packages/deploy/src/lumilake_deploy/docker_client.py` — thread `since` through to docker-py
2424
- `tests/cli/test_deploy_impl.py` — cover the new flag
2525
-->
2626

.github/workflows/check-pr-title.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jobs:
2222
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
2323
with:
2424
version: "0.11.8"
25+
python-version: "3.12"
2526
- name: Validate PR title
2627
env:
2728
PR_TITLE: ${{ github.event.pull_request.title }}

.github/workflows/env-examples.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ jobs:
3333
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
3434
with:
3535
version: "0.11.8"
36+
python-version: "3.12"
3637

3738
- name: Install dependencies
38-
run: uv sync --frozen
39+
run: uv sync --frozen --extra deploy
3940

4041
- name: Verify env example files
4142
run: uv run scripts/dev/check_env_examples.py
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
name: Image Publish
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
tags:
8+
- 'v*'
9+
paths-ignore:
10+
- '**.md'
11+
- 'docs/**'
12+
pull_request:
13+
paths-ignore:
14+
- '**.md'
15+
- 'docs/**'
16+
17+
permissions:
18+
contents: read
19+
20+
concurrency:
21+
group: ${{ github.workflow }}-${{ github.ref }}
22+
cancel-in-progress: true
23+
24+
env:
25+
REGISTRY: ghcr.io
26+
IMAGE_NAME: mlsys-io/lumilake_server
27+
28+
jobs:
29+
build-pr:
30+
name: Build image
31+
if: github.event_name == 'pull_request'
32+
runs-on: ubuntu-latest
33+
permissions:
34+
contents: read
35+
steps:
36+
- name: Checkout
37+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
38+
with:
39+
persist-credentials: false
40+
41+
- name: Set up Buildx
42+
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
43+
44+
- name: Compute image tags
45+
id: tags
46+
env:
47+
SHA: ${{ github.sha }}
48+
run: |
49+
set -eu
50+
short_sha="${SHA::7}"
51+
echo "tags=${REGISTRY}/${IMAGE_NAME}:pr-${short_sha}" >> "$GITHUB_OUTPUT"
52+
53+
- name: Set up uv
54+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
55+
with:
56+
version: "0.11.8"
57+
python-version: "3.12"
58+
# No uv cache restore in a publishing workflow (cache-poisoning).
59+
enable-cache: false
60+
61+
- name: Sync server requirements from uv.lock
62+
run: uv run --frozen scripts/dev/sync_requirements.py --write
63+
64+
- name: Verify lockfile sync was a no-op
65+
run: |
66+
if ! git diff --quiet src/lumilake_server/requirements.txt; then
67+
echo "src/lumilake_server/requirements.txt drifted from uv.lock." >&2
68+
echo "Run: uv run scripts/dev/sync_requirements.py --write" >&2
69+
git --no-pager diff src/lumilake_server/requirements.txt
70+
exit 1
71+
fi
72+
73+
- name: Build image
74+
id: build
75+
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
76+
with:
77+
context: .
78+
file: ./Dockerfile
79+
push: false
80+
tags: ${{ steps.tags.outputs.tags }}
81+
provenance: false
82+
build-args: |
83+
BUILD_VERSION=${{ github.ref_name }}
84+
BUILD_REF=${{ github.sha }}
85+
BUILD_CREATED=${{ github.event.repository.updated_at }}
86+
87+
build-and-push:
88+
name: Build and push multi-arch image
89+
if: github.event_name == 'push'
90+
runs-on: ubuntu-latest
91+
permissions:
92+
contents: read
93+
packages: write # docker/login-action + build-push-action push to GHCR
94+
steps:
95+
- name: Checkout
96+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
97+
with:
98+
persist-credentials: false
99+
100+
- name: Set up QEMU
101+
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
102+
103+
- name: Set up Buildx
104+
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
105+
106+
- name: Log in to GHCR
107+
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
108+
with:
109+
registry: ${{ env.REGISTRY }}
110+
username: ${{ github.actor }}
111+
password: ${{ secrets.GITHUB_TOKEN }}
112+
113+
- name: Compute image tags
114+
id: tags
115+
env:
116+
REF_NAME: ${{ github.ref_name }}
117+
REF_TYPE: ${{ github.ref_type }}
118+
SHA: ${{ github.sha }}
119+
run: |
120+
set -eu
121+
short_sha="${SHA::7}"
122+
tags=("${REGISTRY}/${IMAGE_NAME}:${short_sha}")
123+
# :latest is owned by release-images.yml (gated on env approval).
124+
if [[ "${REF_TYPE}" == "tag" && "${REF_NAME}" == v* ]]; then
125+
tags+=("${REGISTRY}/${IMAGE_NAME}:${REF_NAME}")
126+
elif [[ "${REF_NAME}" == "main" ]]; then
127+
tags+=("${REGISTRY}/${IMAGE_NAME}:dev")
128+
fi
129+
( IFS=,; echo "tags=${tags[*]}" ) >> "$GITHUB_OUTPUT"
130+
131+
- name: Set up uv
132+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
133+
with:
134+
version: "0.11.8"
135+
python-version: "3.12"
136+
# No uv cache restore in a publishing workflow (cache-poisoning).
137+
enable-cache: false
138+
139+
- name: Sync server requirements from uv.lock
140+
run: uv run --frozen scripts/dev/sync_requirements.py --write
141+
142+
- name: Verify lockfile sync was a no-op
143+
run: |
144+
if ! git diff --quiet src/lumilake_server/requirements.txt; then
145+
echo "src/lumilake_server/requirements.txt drifted from uv.lock." >&2
146+
echo "Run: uv run scripts/dev/sync_requirements.py --write" >&2
147+
git --no-pager diff src/lumilake_server/requirements.txt
148+
exit 1
149+
fi
150+
151+
- name: Build and push
152+
id: build
153+
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
154+
with:
155+
context: .
156+
file: ./Dockerfile
157+
platforms: linux/amd64,linux/arm64
158+
push: true
159+
tags: ${{ steps.tags.outputs.tags }}
160+
provenance: false
161+
build-args: |
162+
BUILD_VERSION=${{ github.ref_name }}
163+
BUILD_REF=${{ github.sha }}
164+
BUILD_CREATED=${{ github.event.repository.updated_at }}
165+
166+
- name: Smoke-test pushed image
167+
env:
168+
SHA: ${{ github.sha }}
169+
run: |
170+
set -eu
171+
short_sha="${SHA:0:7}"
172+
image="${REGISTRY}/${IMAGE_NAME}:${short_sha}"
173+
# Plain imports only — routes/jobs.py module-load init needs S3/DB.
174+
docker pull "$image"
175+
docker run --rm --entrypoint /usr/bin/tini "$image" -- \
176+
python -c "import lumilake, lumilake_hook, lumilake_server; print('image-smoke-ok')"

.github/workflows/lint-typecheck.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ jobs:
2626
- name: Set up uv
2727
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
2828
with:
29+
version: "0.11.8"
2930
python-version: "3.12"
3031

3132
- name: Install dependencies
32-
run: uv sync --all-groups
33+
# --all-extras: mypy needs every workspace wheel for cross-imports.
34+
run: uv sync --frozen --all-extras --all-groups
3335

3436
- name: Run pre-commit hooks
3537
run: uv run pre-commit run --all-files --show-diff-on-failure

.github/workflows/package-build.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ concurrency:
2121

2222
jobs:
2323
package-build:
24-
name: Build and smoke-test package
24+
name: Build and smoke-test packages
2525
runs-on: self-hosted
2626
steps:
2727
- name: Checkout
@@ -33,12 +33,16 @@ jobs:
3333
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
3434
with:
3535
version: "0.11.8"
36+
python-version: "3.12"
3637

3738
- name: Install build environment
38-
run: uv sync --all-groups --all-extras --frozen
39+
run: uv sync --all-packages --all-extras --all-groups --frozen
3940

4041
- name: Build distributions
41-
run: uv build --out-dir dist
42+
run: uv build --all-packages --out-dir dist
4243

4344
- name: Verify distributions
4445
run: uv run scripts/ci/check_package_build.py --dist dist
46+
47+
- name: Check distribution metadata
48+
run: uvx twine==6.2.0 check dist/*

0 commit comments

Comments
 (0)