Skip to content

Commit 9506d45

Browse files
authored
Merge pull request #27 from logannye/rosalind/adoption-on-ramp
Adoption on-ramp: prebuilt-binary release + rosalind-budget GitHub Action
2 parents 165a6f1 + 231506d commit 9506d45

6 files changed

Lines changed: 435 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,11 @@ jobs:
160160
test "$code" -eq 3
161161
grep -q "REFUSE" refuse.log
162162
test ! -f examples/data/illumina_toy/refused.vcf
163+
- name: Exercise the rosalind-budget Action (local, from-source binary)
164+
uses: ./
165+
with:
166+
index: examples/data/illumina_toy/reference.idx
167+
alignments: examples/data/illumina_toy/sorted.bam
168+
budget-mb: 4096
169+
binary-path: target/release/rosalind
170+
output: examples/data/illumina_toy/action.vcf

.github/workflows/release.yml

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
name: Release
2+
3+
# Cut a release by pushing a tag: `git tag v0.1.0 && git push origin v0.1.0`.
4+
# `workflow_dispatch` builds the same artifacts WITHOUT publishing a release
5+
# (uploaded as workflow artifacts) — a safe dry run before tagging.
6+
on:
7+
push:
8+
tags:
9+
- 'v*'
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: write # required to create/attach a GitHub Release
14+
15+
jobs:
16+
build:
17+
name: build ${{ matrix.target }}
18+
runs-on: ${{ matrix.os }}
19+
strategy:
20+
fail-fast: false
21+
matrix:
22+
include:
23+
# Fully static Linux binary (no glibc/bzip2/lzma runtime dependency):
24+
# the -sys crates build htslib/bzip2/lzma/zlib from bundled C source.
25+
- target: x86_64-unknown-linux-musl
26+
os: ubuntu-latest
27+
# Native macOS builds (link only system libs present on every Mac).
28+
- target: aarch64-apple-darwin
29+
os: macos-14
30+
- target: x86_64-apple-darwin
31+
os: macos-13
32+
steps:
33+
- uses: actions/checkout@v4
34+
35+
- name: Install musl toolchain (Linux)
36+
if: runner.os == 'Linux'
37+
run: sudo apt-get update && sudo apt-get install -y musl-tools
38+
39+
- uses: actions-rs/toolchain@v1
40+
with:
41+
toolchain: stable
42+
target: ${{ matrix.target }}
43+
profile: minimal
44+
override: true
45+
46+
- name: Cache cargo artifacts
47+
uses: actions/cache@v4
48+
with:
49+
path: |
50+
~/.cargo/registry
51+
~/.cargo/git
52+
target
53+
key: ${{ runner.os }}-release-${{ matrix.target }}-${{ hashFiles('Cargo.lock') }}
54+
55+
- name: Build (release, ${{ matrix.target }})
56+
env:
57+
# musl needs the musl C compiler for the bundled htslib/bzip2/lzma builds.
58+
CC_x86_64_unknown_linux_musl: musl-gcc
59+
run: cargo build --release --target ${{ matrix.target }} --bin rosalind
60+
61+
- name: Stage the bundle
62+
shell: bash
63+
run: |
64+
set -euo pipefail
65+
name="rosalind-${{ matrix.target }}"
66+
stage="dist/$name"
67+
mkdir -p "$stage"
68+
cp "target/${{ matrix.target }}/release/rosalind" "$stage/"
69+
cp README.md CONTRACT.md LICENSE-APACHE LICENSE-MIT "$stage/"
70+
mkdir -p "$stage/examples/data"
71+
cp -R examples/data/illumina_toy "$stage/examples/data/"
72+
tar -C dist -czf "dist/$name.tar.gz" "$name"
73+
# Portable checksum (shasum on macOS, sha256sum on Linux).
74+
if command -v sha256sum >/dev/null 2>&1; then
75+
(cd dist && sha256sum "$name.tar.gz" > "$name.tar.gz.sha256")
76+
else
77+
(cd dist && shasum -a 256 "$name.tar.gz" > "$name.tar.gz.sha256")
78+
fi
79+
80+
- name: Upload workflow artifact (dry run)
81+
uses: actions/upload-artifact@v4
82+
with:
83+
name: rosalind-${{ matrix.target }}
84+
path: |
85+
dist/rosalind-${{ matrix.target }}.tar.gz
86+
dist/rosalind-${{ matrix.target }}.tar.gz.sha256
87+
88+
- name: Attach to the GitHub Release (tag builds only)
89+
if: startsWith(github.ref, 'refs/tags/v')
90+
uses: softprops/action-gh-release@v2
91+
with:
92+
files: |
93+
dist/rosalind-${{ matrix.target }}.tar.gz
94+
dist/rosalind-${{ matrix.target }}.tar.gz.sha256
95+
fail_on_unmatched_files: true
96+
body: |
97+
Prebuilt `rosalind` binaries — the memory contract, runnable in 60 seconds.
98+
99+
```sh
100+
curl -fsSL https://raw.githubusercontent.com/logannye/rosalind/main/install.sh | sh
101+
```
102+
103+
Each tarball bundles the binary, the `illumina_toy` contract-demo fixtures, and the
104+
licenses. See the README "Quickstart (60 seconds)".

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,27 @@ What makes this different:
4848

4949
---
5050

51+
## Quickstart (60 seconds)
52+
53+
Grab a prebuilt binary and watch the contract fire on the bundled data — no toolchain, no build:
54+
55+
```sh
56+
curl -fsSL https://raw.githubusercontent.com/logannye/rosalind/main/install.sh | sh
57+
cd rosalind-*/
58+
59+
# Build a portable index, sort the bundled BAM, then declare a budget and honor it.
60+
./rosalind index --reference examples/data/illumina_toy/reference.fa --output ref.idx
61+
./rosalind sort --input examples/data/illumina_toy/alignments.bam --output sorted.bam
62+
./rosalind plan --index ref.idx --budget-mb 512 # FITS? predicted peak
63+
./rosalind variants --index ref.idx --alignments sorted.bam \
64+
--memory-budget-mb 512 --enforce -o calls.vcf # honors it (exit 3/4)
65+
./rosalind verify --manifest calls.vcf.manifest.json # re-checks the receipt
66+
```
67+
68+
You'll see `plan` predict `[FITS]`, `variants --enforce` print `contract: OK — realized peak … within`, and `verify: OK`. Tighten `--budget-mb` to `1` and `variants --enforce` *refuses up front* (exit 3, no VCF). That is the whole differentiator, in one minute. (Releases are cut from tags; if none is published yet, build from source below.)
69+
70+
---
71+
5172
## What it does today
5273

5374
- **Bounded whole-genome germline calling**`rosalind variants --index` streams a coordinate-sorted BAM over all contigs of a persisted index, calling SNVs to a multi-contig VCF with a working set bounded by coverage. Calls are calibrated and **abstention-aware** (no confident call → no row, rather than a guess).
@@ -84,6 +105,22 @@ The contract is real today: `rosalind plan` predicts before you commit, `--enfor
84105
- Variant calling is **single-sample** (germline) or a **tumor/normal pair** (somatic); calling is SNV-focused, with simple indels in the somatic path.
85106
- The engine runs **single-threaded** today. `--memory-budget-mb` is record-only by default; add `--enforce` to honor it (refuse up front / fail loud — see [CONTRACT.md](CONTRACT.md)).
86107

108+
## The memory contract in *your* CI
109+
110+
Drop the `rosalind-budget` Action into any pipeline to make a declared memory budget a **gate** — the build fails if a whole-genome calling step would breach it. No toolchain on your runner; the Action fetches a prebuilt binary.
111+
112+
```yaml
113+
- uses: logannye/rosalind-budget@v1
114+
with:
115+
index: ref.idx # built by `rosalind index`
116+
alignments: sorted.bam # coordinate-sorted
117+
budget-mb: 4096 # refuse up front (exit 3) / fail after (exit 4) on breach
118+
max-depth: 1000 # optional (default 1000)
119+
max-read-len: 250 # optional (default 250)
120+
```
121+
122+
It runs `plan` (predicts the peak), then `variants --index --enforce` (honors the budget), and uploads the BLAKE3 receipt as a build artifact. This is the one thing a `--max-mem` flag on another caller can't give you: a portable, declarative, **verifiable** memory budget that fails a stranger's build loudly — the contract, enforced where your pipeline already lives. (Available once a release is published; see Quickstart.)
123+
87124
## Roadmap
88125

89126
The core primitive is a streaming, CIGAR-aware pileup column stream; variant calling and custom plugins consume it. Performance work deliberately *follows* the unique capability — the target user needs "it fits and is predictable" before "it's fastest."

action.yml

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
name: 'Rosalind memory budget'
2+
description: 'Honor a declared memory budget for whole-genome germline calling — fail CI on a breach (exit 3 refuse / exit 4 over).'
3+
author: 'logannye'
4+
branding:
5+
icon: 'shield'
6+
color: 'green'
7+
8+
inputs:
9+
index:
10+
description: 'Path to the rosalind index (.idx) built by `rosalind index`.'
11+
required: true
12+
alignments:
13+
description: 'Path to a coordinate-sorted BAM.'
14+
required: true
15+
budget-mb:
16+
description: 'Declared memory budget in MiB. The run is refused up front (exit 3) or fails after (exit 4) if the realized peak exceeds it.'
17+
required: true
18+
max-depth:
19+
description: 'Active-set depth cap (the bound the contract relies on). 0 = uncapped (then --enforce is rejected).'
20+
required: false
21+
default: '1000'
22+
max-read-len:
23+
description: 'Maximum read length assumed and enforced at ingest under --enforce.'
24+
required: false
25+
default: '250'
26+
output:
27+
description: 'Output VCF path.'
28+
required: false
29+
default: 'calls.vcf'
30+
version:
31+
description: 'Rosalind release to download (e.g. v0.1.0), or "latest". Ignored if binary-path is set.'
32+
required: false
33+
default: 'latest'
34+
binary-path:
35+
description: 'Optional path to an existing rosalind binary (skips the download — for air-gapped runners or testing the action against a from-source build).'
36+
required: false
37+
default: ''
38+
39+
runs:
40+
using: 'composite'
41+
steps:
42+
- name: Resolve the rosalind binary
43+
id: bin
44+
shell: bash
45+
env:
46+
VER: ${{ inputs.version }}
47+
BINPATH: ${{ inputs.binary-path }}
48+
run: |
49+
set -euo pipefail
50+
if [ -n "$BINPATH" ]; then
51+
echo "Using provided binary: $BINPATH"
52+
echo "bin=$BINPATH" >> "$GITHUB_OUTPUT"
53+
exit 0
54+
fi
55+
tarball="rosalind-x86_64-unknown-linux-musl.tar.gz"
56+
if [ "$VER" = "latest" ]; then
57+
url="https://github.com/logannye/rosalind/releases/latest/download/$tarball"
58+
else
59+
url="https://github.com/logannye/rosalind/releases/download/$VER/$tarball"
60+
fi
61+
echo "Downloading $url"
62+
curl -fsSL -o "$tarball" "$url"
63+
tar -xzf "$tarball"
64+
echo "bin=$PWD/rosalind-x86_64-unknown-linux-musl/rosalind" >> "$GITHUB_OUTPUT"
65+
66+
- name: Predict the peak (plan)
67+
shell: bash
68+
env:
69+
BIN: ${{ steps.bin.outputs.bin }}
70+
IDX: ${{ inputs.index }}
71+
BUDGET: ${{ inputs.budget-mb }}
72+
run: |
73+
set -euo pipefail
74+
"$BIN" plan --index "$IDX" --budget-mb "$BUDGET"
75+
76+
- name: Honor the memory contract (fails CI on breach)
77+
shell: bash
78+
env:
79+
BIN: ${{ steps.bin.outputs.bin }}
80+
IDX: ${{ inputs.index }}
81+
BAM: ${{ inputs.alignments }}
82+
BUDGET: ${{ inputs.budget-mb }}
83+
MAXDEPTH: ${{ inputs.max-depth }}
84+
MAXREADLEN: ${{ inputs.max-read-len }}
85+
OUT: ${{ inputs.output }}
86+
run: |
87+
set -euo pipefail
88+
"$BIN" variants --index "$IDX" --alignments "$BAM" \
89+
--memory-budget-mb "$BUDGET" \
90+
--max-depth "$MAXDEPTH" --max-read-len "$MAXREADLEN" \
91+
--enforce -o "$OUT"
92+
93+
- name: Upload the memory receipt
94+
if: always()
95+
uses: actions/upload-artifact@v4
96+
with:
97+
name: rosalind-receipt
98+
path: ${{ inputs.output }}.manifest.json
99+
if-no-files-found: ignore
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Adoption On-Ramp (design)
2+
3+
**Status:** DESIGN SPEC — 2026-06-02. **Branch:** `rosalind/adoption-on-ramp` (off `main` `165a6f1`).
4+
From the reflection audit: the differentiator (the live `plan → enforce → verify` contract) is a
5+
60-second demo, but it is **unrunnable by strangers** today — no releases/tags/containers, only a
6+
from-source build behind a C-toolchain gate. Scope (chosen for max tangible value to users +
7+
forkers): **release core + the `rosalind-budget` GitHub Action**; container deferred.
8+
9+
## 1. Goal
10+
11+
Turn "clone → fight the htslib C-toolchain build" into "**run the memory contract in 60 seconds**,"
12+
and give builders a drop-in way to **enforce the contract in their own CI**.
13+
14+
## 2. Honest constraints (stated up front)
15+
16+
- **CI-only verification.** The release workflow and the Action execute on GitHub runners; the Linux
17+
**musl-static** build in particular cannot be run-verified locally (cross-compiling htslib's C from
18+
macOS isn't feasible here). This PR delivers *reviewed* release infrastructure + a **locally-verified
19+
macOS quickstart**; the Linux build is validated when CI first runs.
20+
- **The `v0.1.0` tag is the user's trigger.** `release.yml` fires on a `v*` tag push (plus
21+
`workflow_dispatch` for a manual dry run). Merging this PR releases nothing — cutting the tag (the
22+
outward-facing step) stays the user's call.
23+
- **The Action activates after the first release** (it downloads the release binary). Until `v0.1.0`
24+
exists it is non-functional infrastructure; documented as such.
25+
26+
## 3. Deliverables
27+
28+
### 3a. `.github/workflows/release.yml`
29+
30+
- **Triggers:** `push: tags: ['v*']` and `workflow_dispatch` (manual, for a dry run).
31+
- **Matrix (native runners, no cross-compile):**
32+
- `x86_64-unknown-linux-musl` on `ubuntu-latest`**static** (apt `musl-tools`; `rustup target add`;
33+
the `-sys` crates build htslib/bzip2/lzma/zlib from bundled C, so static linking should succeed).
34+
- `aarch64-apple-darwin` on `macos-14` (native arm64).
35+
- `x86_64-apple-darwin` on `macos-13` (native x86_64).
36+
- **Bundle** per target: the `rosalind` binary + `examples/data/illumina_toy/` (the contract-demo
37+
fixtures) + `README.md` + `CONTRACT.md` + `LICENSE-APACHE` + `LICENSE-MIT`, into
38+
`rosalind-<target>.tar.gz` (+ a `.sha256`).
39+
- **Publish:** attach the tarballs to the GitHub Release for the tag (via `softprops/action-gh-release@v2`).
40+
On `workflow_dispatch` (no tag), build + upload as workflow artifacts only (no release).
41+
- Validate YAML structure locally (`python -c yaml.safe_load` or a manual indentation review); cannot
42+
run the build here.
43+
44+
### 3b. `install.sh` (curl-pipe one-liner)
45+
46+
Detect `uname -s`/`uname -m` → target triple; download
47+
`https://github.com/logannye/rosalind/releases/latest/download/rosalind-<target>.tar.gz`; verify the
48+
`.sha256`; extract to `./rosalind-<version>/`; print the next-step quickstart. Prefer `gh release
49+
download` when `gh` is available (works even if the repo is private); fall back to `curl`. Fail
50+
loudly with a clear message on an unsupported OS/arch. Locally testable: the OS/arch-detection +
51+
error paths (the actual download needs a published release).
52+
53+
### 3c. README "Quickstart (60 seconds)"
54+
55+
A new section near the top: `curl -fsSL …/install.sh | sh``cd` → run the bundled
56+
`plan → variants --enforce → verify` demo on `examples/data/illumina_toy/` (the exact commands already
57+
verified locally end-to-end). Keep the existing from-source "Install & build" section below it for
58+
contributors.
59+
60+
### 3d. `rosalind-budget` GitHub Action (`action.yml` at repo root)
61+
62+
A **composite** action: a consumer adds
63+
```yaml
64+
- uses: logannye/rosalind-budget@v1
65+
with:
66+
index: ref.idx
67+
alignments: sorted.bam
68+
budget-mb: 4096
69+
max-depth: 1000
70+
max-read-len: 250
71+
```
72+
Steps: download the Linux release binary (the action runs on the consumer's Linux runner; no
73+
toolchain needed), run `rosalind variants --index … --alignments … --memory-budget-mb … --enforce
74+
--max-depth … --max-read-len … -o calls.vcf`, so a budget breach **fails the consumer's CI** (exit 3
75+
refuse / exit 4 breach), and upload `calls.vcf.manifest.json` as a build artifact. Inputs:
76+
`index`, `alignments`, `budget-mb` (required), `max-depth` (default 1000), `max-read-len`
77+
(default 250), `version` (default `latest`). Ship:
78+
- `action.yml` (composite).
79+
- `.github/workflows/example-rosalind-budget.yml` — a self-contained example that builds the binary
80+
from source (so the example runs in *this* repo's CI before any release exists), builds the bundled
81+
toy index, and runs the same plan→enforce→verify gate — proving the pattern end-to-end in CI now.
82+
- A README/`docs` section documenting the Action and the "memory contract in your CI" value.
83+
84+
## 4. Out of scope (follow-up)
85+
86+
- **Container image** (Dockerfile + ghcr publish) — a second channel for the same binary; valuable for
87+
HPC/Singularity users, deferred.
88+
- **crates.io publish** — a library-consumer channel; separate.
89+
- Cutting the actual `v0.1.0` tag/release (the user's trigger).
90+
91+
## 5. Self-review
92+
93+
- **Coverage:** release.yml (3a), install.sh (3b), README quickstart (3c), Action + example + docs
94+
(3d). ✓
95+
- **No placeholders:** the musl-static build and the Action's release-download are concrete; the honest
96+
caveat is that they run on CI, not locally. ✓
97+
- **Verification:** macOS quickstart + install.sh detection + YAML structure are checked locally; the
98+
Linux build + the release publish + the Action's download are CI-only (stated). ✓
99+
- **Boundary:** no tag cut; the example workflow proves the Action's *pattern* in this repo's CI today
100+
(build-from-source), decoupled from the not-yet-existent release. ✓

0 commit comments

Comments
 (0)