# repoman CI + binary release — design

**Date:** 2026-06-02
**Status:** Approved (design), pending implementation
**Author:** Chris Tusa <chris.tusa@leafscale.com>

## Goal

Build, test, and release repoman binaries automatically via **Isurus CI**, using
the prebuilt **reef-lang toolchain container** as the build environment. On every
push/PR: build + test. On a version tag: also package a binary bundle and publish
it as an Isurus release asset.

## Context

- repoman is a reef *application* (compiles to a single `build/repoman` binary via
  `reefc`), not the reef compiler. It also ships `profiles/*.yml` and
  `recipes/{docker,claude}/*` that `make install` lays under
  `/usr/local/share/repoman`.
- Isurus CI config is `.isurus-ci.yml` at the repo root, hg-native (`hg push`
  triggers; `hg tag vX && hg push` → tag event). Steps run in `docker:` images;
  `container-registry:` authenticates private pulls; a top-level `release:` block
  attaches `assets:` globs to the release on tag using a `releases:write` token.
- The build image `registry.sharkos.one/reef/reef-builder:latest` already contains
  a **pre-installed reef compiler** (`reefc` on PATH, runtime + clang/lld baked in;
  `reefc doctor` passes). No `opam env` setup is needed — that is only for building
  the compiler from OCaml source, which repoman does not do.
- Reference pipelines: `~/repos/reef-lang/.isurus-ci.yml` (canonical Isurus
  example) and `~/repos/zygaena-project/coral/.woodpecker.yml` (a reef app release,
  pre-Isurus). This design follows reef-lang's structure.

## Decisions

- **Release contents:** binary bundle only (no source tarball).
- **Installer:** a standalone `install.sh` inside the bundle — end users need no
  reefc and no Makefile to install.
- **Arch:** `linux-amd64` only (the reef-builder image and repoman's Incus-host
  target are x86-64). arm64 deferred.
- **Build image tag:** `:latest` (floats with the toolchain, matching reef-lang),
  not a pinned `:0.6.5`.
- **Triggers:** build + test on `[push, pull_request, tag]`; package + release on
  `[tag]` only. Single `when:` filter map, no top-level `branch:` gate (a branch
  gate is incompatible with PR CI — per reef-lang's CLAUDE.md note).
- **Version source of truth:** the `version` field in `reef.toml` (currently
  `0.6.0`). Packaging derives the version from it; the release fires off `$CI_TAG`.

## Components

### 1. `.isurus-ci.yml` (repo root, new)

```yaml
container-registry:
  - url: registry.sharkos.one
    secret: SHARKOS_REGISTRY_CREDS

when:
  event: [push, pull_request, tag]

steps:
  - name: build
    docker: { image: registry.sharkos.one/reef/reef-builder:latest }
    commands:
      - reefc doctor
      - make all
      - ./build/repoman --version
    timeout: 30m

  - name: test
    docker: { image: registry.sharkos.one/reef/reef-builder:latest }
    commands:
      - make test
    timeout: 30m

  - name: package
    docker: { image: registry.sharkos.one/reef/reef-builder:latest }
    when: { event: [tag] }
    commands:
      - ./scripts/package-binary.sh
      - ls -la dist/
    timeout: 30m

release:
  tag: "$CI_TAG"
  title: "repoman $CI_TAG"
  assets:
    - dist/*.tar.gz
    - dist/*.sha256
  secret: ISURUS_RELEASE_TOKEN
  when:
    event: [tag]
```

Notes:
- `make all` lives only in `build`. Steps share the cloned workspace and run
  sequentially (pipeline halts on first failure), so `package` only runs after
  `build`/`test` pass. Each target is also self-sufficient: `make test` compiles
  and runs the test files directly via `reefc run` (no dependency on
  `build/repoman`), and `package-binary.sh` calls `make install`, whose phony
  `build` prerequisite rebuilds the binary. No repeated `make all`.
- `reefc doctor` in `build` gives an early, legible failure if the toolchain image
  is broken.

### 2. `scripts/package-binary.sh` (new, executable)

Produces `dist/repoman-<version>-linux-amd64.tar.gz` and a matching `.sha256`.

Behavior:
1. Derive `VERSION` from `reef.toml` (`grep '^version' | sed`), allow override via
   `$1`.
2. `NAME=repoman-${VERSION}-linux-amd64`, `DIST=${REPOMAN_DIST:-dist}`.
3. Stage the real install tree by reusing the Makefile:
   `make install DESTDIR="$STAGE/$NAME" PREFIX=/usr/local`
   → `$STAGE/$NAME/usr/local/{bin/repoman, share/repoman/...}`.
4. Add `README.md`, `LICENSE`, and a generated `install.sh` (see §3) to
   `$STAGE/$NAME/`.
5. `tar -czf "$DIST/$NAME.tar.gz" -C "$STAGE" "$NAME"`.
6. `sha256sum "$DIST/$NAME.tar.gz" > "$DIST/$NAME.tar.gz.sha256"`.
7. `set -eu`; clean up `$STAGE` on exit.

Rationale for reusing `make install`: the tarball's tree stays byte-identical to a
real install, so there is one definition of the install layout (DRY) — when a new
recipe/profile is added, the bundle picks it up automatically.

### 3. Bundled `install.sh` (inside the tarball)

Relocatable installer; needs no reefc, no make. Run from the unpacked bundle dir:

```sh
#!/bin/sh
set -eu
PREFIX="${PREFIX:-/usr/local}"
here="$(cd "$(dirname "$0")" && pwd)"
# Copy the staged usr/local tree into PREFIX.
cp -a "$here/usr/local/." "$PREFIX/"
echo "repoman installed under $PREFIX"
"$PREFIX/bin/repoman" --version
```

- Honors `PREFIX` (default `/usr/local`); preserves perms (`cp -a`) so the
  launcher recipe stays `0755`.
- Mirrors what `make install` does, minus the build dependency.

### 4. `.hgignore` (edit)

Add `dist/` so packaging output is not accidentally committed.

## Data flow

```
hg push (branch)            -> build + test
hg push (PR)                -> build + test
hg tag vX.Y.Z && hg push    -> build + test + package -> dist/*.tar.gz(+.sha256)
                                                       -> release: attach assets to vX.Y.Z
```

## Prerequisites (operator-side, outside this repo)

Two CI secrets must exist for the repoman repo (or its org) in Isurus
**CI/CD > Secrets**:
- `SHARKOS_REGISTRY_CREDS` — Harbor robot creds JSON
  `{"username":"robot$ci","password":"<token>"}` to pull the builder image.
- `ISURUS_RELEASE_TOKEN` — token with `releases:write` scope.

reef-lang already uses both names; if they are org-scoped, repoman inherits them.

## Testing / verification

- **Local dry runs (no CI needed):**
  - `make all && make test` inside the builder image (or locally) stays green.
  - `./scripts/package-binary.sh` produces a tarball; unpack to a temp dir, run its
    `install.sh` with `PREFIX=$(mktemp -d)`, and confirm `bin/repoman --version`
    prints the bundle version and the claude launcher lands at `0755`.
- **CI smoke:** push a branch → build+test pipeline goes green. Then a throwaway
  tag (or the next real release tag) exercises package + release end to end.

## Out of scope

- Source tarball, multi-arch (arm64), signing/notarization, publishing to any
  index beyond Isurus releases, and container-image publishing for repoman itself.

## Files touched

| File | Change |
|------|--------|
| `.isurus-ci.yml` | new — pipeline + release block |
| `scripts/package-binary.sh` | new, `0755` — binary bundle + sha256 |
| `.hgignore` | add `dist/` |
| `Makefile` | (if needed) confirm `install` works with `DESTDIR`/`PREFIX` — already verified |
| `README.md` | brief "Releases / CI" note (optional) |
