Skip to content

refactor: split packages and add release machinery#5

Merged
timzsu merged 5 commits into
mainfrom
release/ghcr-machinery
May 14, 2026
Merged

refactor: split packages and add release machinery#5
timzsu merged 5 commits into
mainfrom
release/ghcr-machinery

Conversation

@timzsu

@timzsu timzsu commented May 14, 2026

Copy link
Copy Markdown
Collaborator

Purpose

Stand up the OSS release machinery for Lumilake. After this PR the project can be released to PyPI (multiple wheels, OIDC trusted publishing) and GHCR (server image) from a tagged commit, and the CLI works from a plain pip install lumilake[cli] without a source checkout. Mirrors FlowMesh's release-day flow where it makes sense so the two projects converge on one operator playbook.

Changes

Three orthogonal pieces, in order of blast radius:

  • Workspace split. Single lumilake package -> metapackage + 4 published wheels (lumilake-sdk, lumilake-cli, lumilake-deploy, lumilake-hook) + an image-only lumilake_server runtime that never lands on PyPI. SDK is the shared base; CLI and deploy depend on it. lumilake[cli] keeps the full deploy lifecycle; thin pip install lumilake-cli skips Docker/deploy deps and prints install hints for deploy * subcommands.
  • GHCR + PyPI release pipeline. image-publish.yml builds PR images without publishing, and pushes per-commit/per-tag images on main/v* (:sha/:dev/:vX.Y.Z, never :latest). release.yml does PyPI/TestPyPI OIDC publish after building and validating all distributions. release-images.yml verifies the pre-pushed image labels and retags :latest with ghcr environment approval and downgrade protection. check_release_version.py cross-checks 5 pyprojects + 2 __version__ literals; bump_version.py writes them in one pass.
  • Packaged deploy assets + --project-dir. .env.example and compose.yml ship inside lumilake-deploy as package data, resolved via importlib.resources. lumilake deploy -C <dir> (or LUMILAKE_DEPLOY_DIR) selects where env files live; no source checkout needed. SDK resolves FlowMesh container names from .env.flowmesh's slug; down --wipe-archive uses the same slug for volume cleanup.

Design

  • OIDC for PyPI, token-scoped GHCR. PyPI/TestPyPI publish uses GitHub OIDC Trusted Publishing with pypi/testpypi environments. GHCR image push uses the workflow GITHUB_TOKEN with job-level packages: write; PR image builds never receive that permission and never push. The :latest retag is separated behind the ghcr environment.
  • Decoupled image vs PyPI publish. Two separate workflows fire on release: published. PyPI immutability + GHCR :latest retag-after-verify means a half-published release degrades gracefully (re-run release-images.yml for image-only failure; yank + .postN for PyPI). release-images.yml polls the registry up to 30 min to absorb the race with image-publish.yml.
  • Image-only server. Runtime source under src/lumilake_server/ is intentionally not published. check_package_build.py enforces that any wheel containing lumilake_server.* fails CI; it also SHA-checks each wheel's bundled LICENSE matches the repo root.
  • Why FlowMesh-aligned. FlowMesh already has the release-day muscle (check_release_version.py, sentinel-replace digest table append, downgrade-protected retag, two-tier CLI/deploy weight). Cheaper to lift than reinvent; identical operator UX across the two projects.

Test Plan

  • Package checks: uv build --all-packages, twine check, and check_package_build.py validate the metapackage + 4 workspace wheels.
  • CLI/deploy checks cover packaged assets, lumilake deploy -C <dir>, non-checkout deploy behavior, and FlowMesh slug-derived container/volume names.
  • Image publishing is split so PRs build only with read permissions, while main/tag pushes log in to GHCR and smoke-test the pushed image.
  • Release workflows still need their true end-to-end exercise on a v* tag because PyPI Trusted Publisher and GHCR package visibility are operator-side setup.

Test Result

The release workflows themselves will only get a true end-to-end exercise on the first v* tag; their dependency on PyPI Trusted Publisher config + GHCR package visibility/repository access settings is operator-side and documented in docs/RELEASE.md.

Follow-up before first release

  • PyPI Trusted Publisher config for lumilake, lumilake-sdk, lumilake-cli, lumilake-deploy, lumilake-hook (env=pypi, repo=mlsys-io/lumilake_OSS, workflow=release.yml).
  • TestPyPI Trusted Publisher config with env=testpypi.
  • Create pypi / testpypi / ghcr GitHub environments. Add reviewer approval where the repository/org plan exposes environment protection rules.
  • After first image-publish.yml push: flip GHCR package visibility to public + link to repo (Manage Actions access -> Write).

Pre-submission Checklist
  • I have read CONTRIBUTING.md.
  • I have run uv run pre-commit run --all-files and fixed any issues.
  • I have added or updated tests covering my changes (if applicable).
  • I have verified that uv run pytest tests/ --ignore=tests/server passes locally.
  • If I changed the SDK or CLI, I have verified the affected interface works locally.
  • If this is a breaking change, I have prefixed the PR title with [BREAKING] and described migration steps above.
  • I have updated documentation or config examples if user-facing behavior changed.

timzsu and others added 3 commits May 14, 2026 01:22
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>
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>
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>
@timzsu
timzsu force-pushed the release/ghcr-machinery branch from 24bed07 to a6692e5 Compare May 14, 2026 07:32
Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu>
@timzsu
timzsu force-pushed the release/ghcr-machinery branch from a6692e5 to 52f3224 Compare May 14, 2026 07:47
@timzsu timzsu changed the title chore: add release workflows refactor: split packages and add release machinery May 14, 2026
Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu>
@timzsu
timzsu force-pushed the release/ghcr-machinery branch from 5196951 to 8dfe349 Compare May 14, 2026 07:59
@timzsu
timzsu merged commit 9fb69b6 into main May 14, 2026
12 checks passed
@timzsu
timzsu deleted the release/ghcr-machinery branch May 14, 2026 08:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant