Metadata-Version: 2.4
Name: datahenge-cairn
Version: 0.2.0
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.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-btu-v16"
series = "v16"

[cairn.frappe]
url = "https://github.com/frappe/frappe"
ref = "version-16"

[[cairn.apps]]
name = "erpnext"
url = "https://github.com/frappe/erpnext"
ref = "version-16"
```

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 **[docs/technical/CONFIGURATION.md](docs/technical/CONFIGURATION.md)** for the full manifest schema, 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), sharing
`/etc/cairn` across several operators, and how a target's `/etc/cairn/adopt.toml`
descriptor comes from `cairn-adopt examine` rather than being hand-authored.

### 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 new-tag staging --latest       # point staging at the newest build
cairn-build retag production --from staging --yes   # promote staging's image to production
cairn-build retag 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.
