Metadata-Version: 2.4
Name: datahenge-cairn
Version: 0.3.1
Summary: Wrapper around frappe_docker for reproducible ERPNext image builds and pull-based deploys on a single VPS.
Project-URL: Homepage, https://github.com/Datahenge/cairn
Project-URL: Repository, https://github.com/Datahenge/cairn
Project-URL: Issues, https://github.com/Datahenge/cairn/issues
Project-URL: Changelog, https://github.com/Datahenge/cairn/blob/main/docs/CHANGELOG.md
Author-email: Brian Pond <brian@datahenge.com>
License-Expression: MIT
License-File: LICENSE
Keywords: deployment,devops,docker,erpnext,frappe
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: System :: Installation/Setup
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.11
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: twine>=6.0; extra == 'dev'
Requires-Dist: ventwig<0.3,>=0.2; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
Description-Content-Type: text/markdown

# cairn

A thin, opinionated wrapper around [`frappe/frappe_docker`](https://github.com/frappe/frappe_docker)
that makes running a custom ERPNext deployment (Frappe + ERPNext + custom apps) on a
single VPS **reproducible, immutable, and low-thought** — without ever modifying
upstream.

Distributed as **`datahenge-cairn`** on PyPI — installs the **`cairn-build`**,
**`cairn-adopt`**, and **`cairn-registry`** commands.

Two pillars: **reproducible custom image builds** and a **pull-based deploy lifecycle**
(git ref → image tag → running stack, with image-only rollback). A strict data-plane
boundary keeps cairn out of your databases and volumes entirely — it ships code, not
data.

> _Cairn: a trail marker of stacked stones. Each deploy drops a durable marker
> (ref → resolved commits → image tag → digest) you can navigate back to._

📖 **[Documentation](https://datahenge.github.io/cairn/)**

## Three roles, one install

cairn is three commands, one package, told apart by what's configured on a machine rather
than by what's installed:

| | **Builder** — `cairn-build` | **Target** — `cairn-adopt` | **Registry** — `cairn-registry` |
| --- | --- | --- | --- |
| Does | Builds images, pushes them, moves environment pointers | Polls for its pointer, pulls the image, converges the running stack | Provisions and operates a local OCI registry: lifecycle, retention, garbage collection |
| Reads | `cairn.toml` (the manifest) | `/etc/cairn/adopt.toml` (a descriptor, generated by `cairn-adopt examine`) | `/etc/cairn/registry.toml` (optional — built-in defaults otherwise) |
| Needs | Docker Engine v23+ or podman v4+, `git` | Docker Engine + `docker compose` | Docker Engine + `docker compose`, `openssl` |
| Credential | push access to the registry | pull-only | none — reads no manifest, no `[cairn.declared_environments]` |

The same `pip install datahenge-cairn` installs all three — a target simply never has a
reason to run `cairn-build`'s commands, and its pull-only registry credential means it
couldn't push or retag even if it did. `cairn-registry` is only needed at all if you choose
the self-hosted local-registry option (see [Where your images live](#where-your-images-live))
— it is independent of the other two roles and is sometimes colocated with a builder or
target, sometimes not.

## Configuration

One manifest declares the image (`cairn.toml`, committed with the deployment); machine-
local build settings, if you need any, live separately and are never shared:

```toml
# cairn.toml
[cairn]
image_name = "erpnext-v16"
series = "v16"

[cairn.frappe]
url = "https://github.com/frappe/frappe"
ref = "v16.25.0"

[[cairn.apps]]
name = "erpnext"
url = "https://github.com/frappe/erpnext"
ref = "v16.26.1"
```

`ref` takes a tag or a branch. A tag is reproducible — the same tag always resolves to the
same commit. A branch such as `version-16` is a moving pointer: it always builds that
branch's newest commit, which is convenient if what you actually want is "always the
latest release," at the cost of two builds from the same manifest potentially producing
different images. cairn warns, but does not refuse, when a manifest pins to a branch.

Every command names its manifest explicitly — `--manifest <path>`, or `$CAIRN_MANIFEST` if
you'd rather not repeat the flag. cairn never searches a directory for one: on a shared
machine, "the nearest `cairn.toml`" is a silent way to act on the wrong deployment, not a
convenience. There's no standalone scaffolding command — `cairn-build setup --client <name>`
writes a starter manifest to `/srv/cairn/<name>/cairn.toml`, but only as one step of
provisioning a whole build machine, and only if none exists there yet. Otherwise, hand-write
one, starting from the example above.

See the published reference for the full manifest schema
([cairn.toml](https://datahenge.github.io/cairn/reference/manifest/)), the machine-local
`/etc/cairn/builder.toml` layer and its `CAIRN_*` environment-variable overrides — what
each key means, how they're created, and how precedence works — and sharing `/etc/cairn`
across several operators
([builder.toml](https://datahenge.github.io/cairn/reference/builder-config/)), and how a
target's `/etc/cairn/adopt.toml` descriptor comes from `cairn-adopt examine` rather than
being hand-authored
([target descriptor](https://datahenge.github.io/cairn/reference/target-descriptor/)).

### Where images are pushed

Which registry you use, and who owns the credential, is worth thinking about deliberately
— especially when you're building images for a client. See
**[docs/technical/ABOUT_REGISTRIES.md](docs/technical/ABOUT_REGISTRIES.md)** for the tradeoffs, and
**[docs/technical/ABOUT_GHCR.md](docs/technical/ABOUT_GHCR.md)** for GitHub's registry specifically. cairn itself is
registry-agnostic and stores no credentials — authenticate with `docker login` or `podman
login` before pushing.

## How to use

The examples below assume `$CAIRN_MANIFEST` is already exported for the session (e.g.
`export CAIRN_MANIFEST=/srv/acme/cairn.toml`) — add `--manifest <path>` to any of them
instead if you'd rather not.

**On a builder:**

```
cairn-build doctor                 # confirm the machine can actually build
cairn-build build                  # build the image declared by the manifest
cairn-build build --push           # ...and upload it
cairn-build images --local         # what's on this machine, and which builds are superseded
cairn-build prune                  # remove superseded local images (keeps build-cache layers)
```

Moving an environment's pointer — which is how you deploy, promote, or roll back — never
rebuilds or re-pulls anything; it just writes a tag in the registry:

```
cairn-build assign-tag staging --latest       # point staging at the newest build (creates
                                               # the pointer the first time, moves it after)
cairn-build assign-tag production --from staging --yes   # promote staging's image to production
cairn-build assign-tag production --previous             # roll back production one image
cairn-build images                                        # what the registry holds, and which tags point where
```

**On a target:**

```
cairn-adopt examine             # describe this host's running stack (one-time, or after a manual change)
cairn-adopt systemd-units       # print the reconcile service + timer; review, then install them
cairn-adopt reconcile --dry-run # see what would change
cairn-adopt reconcile           # converge to whatever the environment's pointer says
```

`reconcile` is idempotent and meant to run on a timer — with nothing to do, it does
nothing. It never rolls back on failure; it stops and reports, because a failed `bench
migrate` is not something to silently reverse.

**On a registry host** (only if you self-host — see [Where your images
live](#where-your-images-live)):

```
cairn-registry doctor            # confirm the registry is reachable, its cert is valid, disk has room
cairn-registry images            # what's in the registry, and which tags point where
cairn-registry prune --dry-run   # what retention would delete, without deleting anything
cairn-registry gc --dry-run      # what garbage collection would reclaim
```

## Where your images live

cairn builds an image and puts it in a container registry; your deployment targets pull from
there. cairn is registry-agnostic and assumes nothing — but **which** registry is not a neutral
choice when you build software for clients:

📦 **[docs/technical/ABOUT_REGISTRIES.md](docs/technical/ABOUT_REGISTRIES.md)** — start here. The image belongs in the account
that owns the source; your credential should reach the engagement's images and nothing else; and
what each option costs at ERPNext image sizes. Includes what to ask a client for.

🐙 **[docs/technical/ABOUT_GHCR.md](docs/technical/ABOUT_GHCR.md)** — GitHub's registry in detail: tokens, scopes, how narrow
access can be, visibility, and the deletion rule that is genuinely surprising. One option among
several, not the default.

If you choose to self-host — cost dominates and off-host rollback history is genuinely not
needed — `cairn-registry` provisions and operates that registry: lifecycle, retention, and
garbage collection, so disk use stays bounded. See `cairn-registry --help`.

> **You should never be the sole owner of a client's image.** If the relationship ends, they must
> still be able to deploy and roll back software they own. cairn is built so the registry can be
> an account you do not control, and so your push credential can be scoped to one repository.
