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.

  1. 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
  1. Push the repo to GitHub.
  2. Build and publish the control API image with GitHub Actions:
git push origin main

The Build Control Image workflow publishes:

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
  1. Configure PyPI Trusted Publishing once for the luma-infra project:

The package distribution name is luma-infra; the installed console command remains luma.

  1. 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.

  1. 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:

  1. capture the public-route baseline;
  2. 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;
  3. let the page start the manager rollout only after that preparation succeeds and reconnect automatically;
  4. verify the automatic post-update route sentinel;
  5. update only non-manager nodes whose reported agent version differs from the release;
  6. 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.