release pipeline for lichen
README.md

Release Pipeline #

Automated release pipeline for lichen. Builds static musl binaries, publishes them as tangled artifacts, pushes Docker images, updates YunoHost and Helm packages.

This repo is checked out inside the lichen repo, as <lichen>/release — the pipeline builds from its parent directory.

Quick start #

ssh root@release-runner     # or ssh root@100.67.126.34
cd /root/lichen
git pull origin main        # get latest code
/root/run-release.sh run <version>

Example: /root/run-release.sh run 0.2.0

Locally, release/lmrelease.sh <version> does the tag, build, publish and Docker steps inside nix develop .#release.

How publishing works #

Tangled has no upload API of its own. A release artifact is an sh.tangled.repo.artifact record in the releasing account's atproto repo, with the binary stored as a blob on that account's PDS. The record points at the sh.tangled.repo record for lichen.page and at the sha1 of the annotated tag object, and the tangled appview picks it up off the firehose.

That means two things:

  • publishing needs an atproto app password, not a forge token
  • release tags must be annotated (git tag -a) — a lightweight tag has no tag object for an artifact to hang off

Artifacts show up at https://tangled.org/notplants.bsky.social/lichen.page/tags and download from https://tangled.org/notplants.bsky.social/lichen.page/tags/<tag-object-sha>/download/<file>.

First-time setup on the release runner #

1. Create .env #

cp /root/lichen/release/.env.example /root/lichen/release/.env

Fill in:

TANGLED_APP_PASSWORD=<app password for notplants.bsky.social>
DOCKER_USERNAME=notplants
DOCKER_PASSWORD=<your docker hub token>

To create an app password: https://bsky.app/settings/app-passwords

2. Set up the tangled SSH key #

The pipeline pushes release tags to the tangled and origin remotes, so the VM needs a key with push access:

ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N "" -C "release-runner"
cat /root/.ssh/id_ed25519.pub
# Add this key to your tangled account

The YunoHost package repo still lives on Codeberg, so the ynh step also needs push access there (SSH key, or CODEBERG_TOKEN in test mode).

Making a release #

1. Prepare #

ssh root@release-runner
cd /root/lichen
git pull origin main

Make sure HEAD is the commit you want to release.

2. Run the pipeline #

/root/run-release.sh run <version>

Pass the version without a v prefix. The pipeline adds it automatically.

Example: /root/run-release.sh run 0.2.0 creates tag v0.2.0.

3. What happens #

The pipeline runs these steps in order:

  1. Build — cross-compiles 4 static (musl) lichen binaries:

    • minimum (no optional features) × x86_64, aarch64
    • full (shell, git, atproto, oidc, hugo, mdbook) × x86_64, aarch64
    • Binaries are gzip-compressed
  2. Tangled — creates annotated tag v<version>, pushes it to origin and tangled, uploads the compressed binaries to the PDS and attaches them to the tag as sh.tangled.repo.artifact records

  3. Docker — builds and pushes two Docker images to Docker Hub:

    • notplants/lichen-min:<version> (Alpine + minimum binary)
    • notplants/lichen-full:<version> (Alpine + full binary + hugo, git, bubblewrap)
  4. YunoHost — updates manifest.toml and apps.json in the r-lichen_ynh package repo with new version, URLs, and checksums

  5. Helm — updates Chart.yaml appVersion and values.yaml image tag, commits and pushes to the main repo

  6. Tauri (optional) — builds Linux desktop app (.deb, .rpm) if cargo-tauri is installed

4. If something fails #

The pipeline is idempotent and resumable. Just rerun the same command:

/root/run-release.sh run <version>

It checks state.json and skips completed steps. To force a full re-run:

/root/run-release.sh run <version> --force

5. Verify #

  • Tangled artifacts: https://tangled.org/notplants.bsky.social/lichen.page/tags
  • Docker: docker pull notplants/lichen-full:<version>
  • Helm: check charts/lichen/values.yaml in the repo

Running individual steps #

/root/run-release.sh build <version>     # just compile binaries
/root/run-release.sh publish <version>   # just tag + upload tangled artifacts
/root/run-release.sh docker <version>    # just build + push docker
/root/run-release.sh ynh <version>       # just update yunohost package
/root/run-release.sh helm <version>      # just update helm chart
/root/run-release.sh tauri <version>     # just build desktop app

Test mode #

To test against disposable test accounts:

RELEASE_MODE=test /root/run-release.sh run 0.0.1-test

This uses the nptest2 Docker Hub account and the nptest Codeberg account for the YunoHost package. Set TANGLED_HANDLE, TANGLED_REPO and TANGLED_APP_PASSWORD in .env to point publishing at a throwaway tangled repo.

What's where #

Path Purpose
/root/run-release.sh Wrapper script (sets PATH and RELEASE_MODE)
/root/lichen/release/lmrelease.sh Local tag + build + publish + docker run
/root/lichen/release/.env Credentials (not in git)
/root/lichen/release/release/ Python pipeline scripts
/root/lichen/release/releases/<version>/ Built binaries
/root/lichen/release/state.json Pipeline state (which steps completed)
/root/lichen/release/logs/<version>.log Build logs
/root/lichen/release/docker/Dockerfile.prebuilt Docker image definition
/root/lichen/release/packages/lichen_ynh/ Cloned YunoHost package repo

Production repos #

What Location
Tangled (code + release artifacts) notplants.bsky.social/lichen.page
Tangled (this repo) notplants.bsky.social/lichen-releaser
Artifact blobs the PDS of notplants.bsky.social
Docker Hub notplants/lichen-min, notplants/lichen-full
YunoHost notplants/r-lichen_ynh (on Codeberg)
Helm chart charts/lichen/ in the main repo

Unified executable #

Release assets are lichen-{minimum,full}-{x86_64,aarch64}-linux.gz. Both variants build lichen-cli --bin lichen --no-default-features; only the full variant adds the configured optional features. Docker installs /usr/local/bin/lichen. The minimal image runs lichen server --site /data run; the full image runs lichen server --multi --root-dir /data run. Use matching deployment entrypoints when adopting the new assets. The YunoHost step also migrates the package executable name, service, and install/upgrade/restore scripts to the unified commands. Existing releases retain their original names.

Run the focused release-contract tests with python3 -m unittest discover -s tests.