Release#
Luma can be distributed without asking users to clone the repository.
Shared source validation#
Pull requests, Python package releases and Control image releases run the same source gate. After installing the Python test extras and dashboard dependencies:
python -m pip install -e ".[test]"
npm ci
bash scripts/check-luma.sh
The gate checks synchronized versions, the generated CLI reference, all root
unittest tests, Dashboard behavior tests, types/build and whitespace. After changing CLI
arguments, run python scripts/generate-cli-reference.py and commit the updated
reference. LAE keeps its separate workspace/CI (cd lae && make check). These
checks validate source and build artifacts; they do not validate a live cluster.
Both publishing workflows additionally reject release tags that do not match
the package version, before any image or package publication.
Recommended Release#
- Bump the package version before committing code that should be distinguishable by
luma version:
python scripts/bump-version.py
Use --minor, --major, or --set 0.2.0 when patch bumping is not appropriate. Verify without changing files:
python scripts/bump-version.py --check
- Push the repo to GitHub.
- Build and publish the control API image with GitHub Actions:
git push origin main
The Build Control Image workflow publishes:
ghcr.io/liutianjie/luma-control:latestfrommainghcr.io/liutianjie/luma-control:main-<sha>frommainghcr.io/liutianjie/luma-control:<tag>fromv*tagsghcr.io/liutianjie/luma-control:sha-<7-char-sha>from an authorizedworkflow_dispatchrun on any branch or tag
The manual tag is commit-scoped. It does not add latest to a topic branch or
change the existing main-*/release-tag channels. Run the workflow only after
the candidate ref is pushed, and verify that the completed run's headSha
matches the commit you intend to install:
BRANCH=codex/lae-foundation
git fetch origin "$BRANCH"
FULL_SHA="$(git rev-parse "origin/$BRANCH^{commit}")"
SHORT_SHA="$(printf '%s' "$FULL_SHA" | cut -c1-7)"
gh workflow run control-image.yml --ref "$BRANCH"
gh run list --workflow control-image.yml --branch "$BRANCH" \
--event workflow_dispatch --limit 5 \
--json databaseId,headSha,status,conclusion
RUN_ID=<database-id-for-the-matching-head-sha>
gh run watch "$RUN_ID" --exit-status
test "$(gh run view "$RUN_ID" --json headSha --jq .headSha)" = "$FULL_SHA"
test "$(gh run view "$RUN_ID" --json conclusion --jq .conclusion)" = success
CONTROL_IMAGE="ghcr.io/liutianjie/luma-control:sha-$SHORT_SHA"
docker buildx imagetools inspect "$CONTROL_IMAGE"
Do not select a run only because it is the newest one; branch concurrency and
re-runs can make that assumption wrong. A workflow_dispatch run can target a
non-default branch with --ref, but the workflow itself must already be
available to GitHub Actions from the repository's default branch.
Candidate Manager Upgrade And Rollback#
First JSON-to-SQLite upgrade#
The first release using SQLite requires a short Control maintenance window.
Pause new deploy/build requests and wait for active builds before starting the
update. Capture the current Control job spec/image, CLI install ref, ingress
baseline and a private backup of /opt/luma/control, /opt/luma/luma.yaml and
the external configuration described in Control storage.
The updated CLI reads legacy configuration without importing it during role
detection or image preparation. After prefetching the Control image, the
installer stops only the luma-control Nomad job and waits until its allocations
are confirmed terminated. Unknown, lost or still-running allocations, a Nomad
query failure or a timeout abort the import. Nomad, Traefik and application jobs
are not stopped by this storage cutover.
After the old writer exits, the installer saves a private checkpoint in a
control-pre-sqlite-* sibling directory, including the final legacy state,
Control job spec and Luma configuration. It imports the latest JSON, merges only
the installation fields and starts the new Control with AutoRevert=false.
The pending cutover marker preserves this safeguard across a failed update
retry. Fresh installs and later SQLite-to-SQLite updates do not stop Control for
migration; normal compatible updates retain automatic rollback.
Do not run new maintenance commands or manually call load_state() against a
live legacy Manager before this fenced update: those entry points may initialize
SQLite. If import or startup fails, the updater does not automatically restart
the JSON-writing image. Retry the compatible release after fixing the reported
problem.
The job-only rollback commands below are valid only between versions using
the same state format. Never revert directly to a JSON-writing Control after
SQLite cutover. It would read frozen JSON and lose subsequent state changes.
To return to the pre-SQLite release, stop the new Control, preserve its SQLite
directory separately, and restore the checkpoint's legacy control directory
and matching configuration into a fresh directory. With no new writer running,
point the old job at that restored state (or swap the stopped Manager's state
directory), install the recorded old CLI and run the saved old job spec. This
restores the checkpoint time and discards later Control changes; reconcile any
application operations since that checkpoint before resuming clients. Prefer a
fixed SQLite-compatible release when those later changes must be retained.
Compatible release upgrades#
Use the same full commit for the CLI source archive and the commit-scoped
Control image. install-luma.sh accepts a full 40-character Git commit as
LUMA_INSTALL_REF, so manager and fleet updates do not need a mutable branch
name. Before changing the manager, record the current Nomad job version and
image in the change record:
PREVIOUS_JOB_VERSION="$(nomad job inspect -json luma-control | jq -er .Version)"
PREVIOUS_CONTROL_IMAGE="$(nomad job inspect -json luma-control | \
jq -er '.TaskGroups[] | select(.Name == "luma-control") | .Tasks[] | select(.Name == "luma-control") | .Config.image')"
nomad job history -p luma-control
Also record a known-good Git install ref, normally the current release tag, as
PREVIOUS_INSTALL_REF. Then run the candidate update on the manager. Keep the
LAE Control environment exported in the same shell when this is an LAE-aware
Control rollout:
FULL_SHA=<verified-40-character-commit>
SHORT_SHA="$(printf '%s' "$FULL_SHA" | cut -c1-7)"
CONTROL_IMAGE="ghcr.io/liutianjie/luma-control:sha-$SHORT_SHA"
export LUMA_CONTROL_IMAGE="$CONTROL_IMAGE"
luma update manager --install-ref "$FULL_SHA" --domain luma.itool.tech
luma version --control-url https://luma.itool.tech
curl --fail --silent --show-error https://luma.itool.tech/v1/health
nomad job status luma-control
The current manager update path preserves Control state and user jobs, but it
does reconcile firewall TCP relay ports, Traefik when the manager has the
edge role, the watchdog, installed config/state, and the luma-control Nomad
job. It does not restart Docker or Nomad, run egress setup, or redeploy user
applications. Treat it as a control-plane maintenance change and keep the
pre-change values above until post-update checks pass.
Compatible upgrades use Nomad AutoRevert, but operators must still verify the
public health endpoint and the running image. If the new Control allocation is
unhealthy, restore the prior Nomad job immediately:
nomad job revert luma-control "$PREVIOUS_JOB_VERSION"
nomad job status luma-control
curl --fail --silent --show-error https://luma.itool.tech/v1/health
That first rollback restores the Control job spec; it does not restore the locally installed CLI. After service recovery, return both CLI and image to the recorded release:
PREVIOUS_INSTALL_REF=<known-good-tag-or-40-character-commit>
export LUMA_CONTROL_IMAGE="$PREVIOUS_CONTROL_IMAGE"
luma update manager --install-ref "$PREVIOUS_INSTALL_REF" --domain luma.itool.tech
If the candidate CLI itself cannot run the rollback, reinstall the known-good CLI first, then repeat the manager refresh:
curl -fsSL https://raw.githubusercontent.com/LiuTianjie/luma/main/scripts/install-luma.sh | \
LUMA_INSTALL_REF="$PREVIOUS_INSTALL_REF" sh
export LUMA_CONTROL_IMAGE="$PREVIOUS_CONTROL_IMAGE"
~/.local/bin/luma update manager --install-ref "$PREVIOUS_INSTALL_REF" \
--domain luma.itool.tech
- Configure PyPI Trusted Publishing once for the
luma-infraproject:
- owner:
LiuTianjie - repository:
luma - workflow:
pypi.yml - environment:
pypi
The package distribution name is luma-infra; the installed console command remains luma.
- Create a tag to publish a versioned image, GitHub archive, and PyPI package:
git tag v0.1.365
git push origin main --tags
The Publish Python Package workflow builds wheel and sdist, runs twine check, and publishes with pypa/gh-action-pypi-publish@release/v1 through OIDC. Do not store a long-lived PYPI_API_TOKEN secret.
- CI users install with:
python -m pip install "luma-infra==0.1.365"
Interactive users can still install with:
export LUMA_INSTALL_REF=v0.1.365
curl -fsSL "https://raw.githubusercontent.com/LiuTianjie/luma/$LUMA_INSTALL_REF/scripts/install-luma.sh" -o /tmp/install-luma.sh && sh /tmp/install-luma.sh
The installer downloads the GitHub archive for that tag, prepares an isolated runtime under ~/.local/share/luma/releases/, installs the Python package, writes ~/.local/bin/luma, and adds ~/.local/bin to the user's shell profile when needed.
Roll Out A Published Release From Dashboard#
After both package and Control-image workflows succeed, open Dashboard → Infrastructure → Nodes → System maintenance. Use the immutable tag as the release ref and the same-tag Control image. The supported order is:
- capture the public-route baseline;
- confirm the Control update; the declared Builder first copies the external image into the internal registry through its managed egress proxy and verifies the digest;
- let the page start the manager rollout only after that preparation succeeds and reconnect automatically;
- verify the automatic post-update route sentinel;
- update only non-manager nodes whose reported agent version differs from the release;
- retry any failed or interrupted image, manager, or node operation from the same page.
Control-image preparation and fleet operations are persisted by Control. The image preparation uses the Builder's control-image-mirror-v1 capability and the configured registryHost / pushHost; its proxy is never exposed to the browser. Manager updates run in an independent transient systemd unit, so refreshing the manager node agent cannot terminate the rollout that started it. A fleet node is successful only after the installer finishes and a new heartbeat reports the target release version. CLI update commands remain break-glass and first-adoption fallbacks; they are not required for normal releases.
Users can uninstall the local CLI with:
curl -fsSL https://raw.githubusercontent.com/LiuTianjie/luma/main/scripts/uninstall-luma.sh | sh
Use --purge to also remove local config and login contexts:
curl -fsSL https://raw.githubusercontent.com/LiuTianjie/luma/main/scripts/uninstall-luma.sh | sh -s -- --purge
The uninstall script is intentionally local-only. It does not remove server runtime components such as Docker, Nomad, Traefik, Luma Control, deployed services, or /opt/luma.
The default control image is ghcr.io/liutianjie/luma-control:latest. If you want a fully pinned bootstrap, set this in luma.yaml before running manager bootstrap:
defaults:
images:
lumaControl: ghcr.io/liutianjie/luma-control:v0.1.365
Latest Channel#
For early testing, users can install main:
curl -fsSL https://raw.githubusercontent.com/LiuTianjie/luma/main/scripts/install-luma.sh | sh
This is convenient but less reproducible than a tag. For real users, prefer a version tag.
For CI, prefer the pinned PyPI package:
python -m pip install "luma-infra==0.1.365"
Custom Host Or Fork#
Use these environment variables when the code is hosted somewhere else:
curl -fsSL https://example.com/install-luma.sh | \
LUMA_REPO_URL=https://github.com/acme/luma \
LUMA_INSTALL_REF=v0.1.365 \
sh
Use LUMA_ARCHIVE_URL to bypass GitHub archive URL conventions completely.
PyPI Package Checks#
Before tagging, verify the package locally:
rm -rf dist
python -m pip install --upgrade build twine
python -m build
python -m twine check dist/*
python -m venv /tmp/luma-package-test
. /tmp/luma-package-test/bin/activate
python -m pip install dist/*.whl
luma version --local
The package includes runtime stack templates and dashboard assets as package data. The one-line installer remains useful for local preflight, venv creation, and PATH setup. Host-level changes such as Linux DNS repair are handled by manager bootstrap or node join, not by a CLI-only install.