Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions .github/workflows/cli-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,12 @@ name: cli-e2e
# dispatch — they no longer run on every CLI code change.

on:
push:
branches: [main]
paths:
- "cli/**"
- "internal/client/**"
- "internal/ids/**"
- "backend/cmd/orva/**"
- "scripts/install-cli.sh"
- "scripts/install-cli.ps1"
- "test/cli/**"
- ".github/workflows/cli-e2e.yml"
# Source validation (unit + cross-build) runs on PRs. The install jobs
# download the RELEASED CLI, so they run on release:published (against the
# freshly-published artifacts) — not on main-push, which would race the
# release cut and 404 on "latest".
release:
types: [published]
pull_request:
paths:
- "cli/**"
Expand Down Expand Up @@ -123,6 +118,7 @@ jobs:
# of Linux; we don't want one on every CLI code change.
if: |
needs.changes.outputs.installers == 'true'
|| github.event_name == 'release'
|| github.event_name == 'schedule'
|| github.event_name == 'workflow_dispatch'
env:
Expand Down Expand Up @@ -157,6 +153,7 @@ jobs:
# Same gating as install-macos. Windows runners are 2× Linux cost.
if: |
needs.changes.outputs.installers == 'true'
|| github.event_name == 'release'
|| github.event_name == 'schedule'
|| github.event_name == 'workflow_dispatch'
env:
Expand Down
17 changes: 6 additions & 11 deletions .github/workflows/install-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,12 @@ name: install-e2e
# the scripts pass lint and focuses on actually running them.

on:
push:
branches: [main]
paths:
- "scripts/install.sh"
- "scripts/uninstall.sh"
- "scripts/orva.service"
- "scripts/orva.openrc"
- "scripts/entrypoint.sh"
- "Dockerfile"
- "test/install/**"
- ".github/workflows/install-e2e.yml"
# Runs AFTER a release publishes — these download the released binary, so
# they must not run on main-push: during a release cut the old release is
# already gone and the new one isn't published yet, so "latest" 404s. On
# release:published they validate the freshly-published artifacts.
release:
types: [published]
pull_request:
paths:
- "scripts/install.sh"
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,38 @@ env:
IMAGE_NAME: ghcr.io/harsh-2002/orva

jobs:
# ── 0. Validate — the release gate ───────────────────────────────────
# The release is the LAST step: nothing is built or published until this
# is green. Mirrors ci.yml's go job (vet + race tests over the full
# module) so a tag can never ship code whose tests fail. The build +
# publish jobs below all depend on it.
validate:
name: validate (vet + race tests)
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "22"
cache: ""
- uses: actions/setup-go@v6
with:
go-version: "1.26.x"
cache: false
- name: build ui + embed adapters
# //go:embed needs ui_dist + adapters populated before vet/test.
run: |
make embed
make adapters-embed
- name: go vet
run: go vet ./...
- name: go test
run: go test -count=1 -race ./...

# ── 1. Go binary: matrix amd64 + arm64 ───────────────────────────────
build-orva:
name: build orva (${{ matrix.goarch }})
needs: validate
runs-on: ubuntu-24.04
strategy:
matrix:
Expand Down Expand Up @@ -114,6 +143,7 @@ jobs:
# Windows builds get the .exe suffix.
build-cli:
name: build orva-cli (${{ matrix.goos }}/${{ matrix.goarch }})
needs: validate
runs-on: ubuntu-24.04
strategy:
matrix:
Expand Down Expand Up @@ -201,6 +231,7 @@ jobs:
# (Linux ≥ 3.2, which covers every system anyone deploys this on).
build-nsjail:
name: build nsjail (${{ matrix.goarch }})
needs: validate
runs-on: ${{ matrix.runner }}
container: debian:bookworm-slim
strategy:
Expand Down Expand Up @@ -247,6 +278,7 @@ jobs:
# ── 3. Language rootfs tarballs ───────────────────────────────────────
build-rootfs:
name: rootfs ${{ matrix.runtime }} (${{ matrix.goarch }})
needs: validate
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
Expand Down Expand Up @@ -405,6 +437,7 @@ jobs:
# two digests into both `:latest` and the immutable `:${tag}`.
docker-image:
name: docker image (${{ matrix.goarch }})
needs: validate
runs-on: ${{ matrix.runner }}
strategy:
matrix:
Expand Down
24 changes: 13 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,19 @@ Dockerfile Multi-stage image (dev and production — single file)

## Release Policy

**One active release at a time.** When cutting a release:
1. Delete the existing GitHub release and its tag first
2. Tag as `vYYYY.MM.DD` (today's date, zero-padded) and push — the workflow does the rest

```bash
gh release delete v<old-tag> --yes
git tag -d v<old-tag> && git push origin --delete v<old-tag>
git tag v2026.05.03 && git push origin v2026.05.03
```

The release workflow builds `ghcr.io/harsh-2002/orva:<tag>` + `:latest` (multi-arch), all CLI binaries, rootfs tarballs, and checksums automatically on any `v*` tag push.
**One active release at a time** — and **validate first, release last**. Order matters: never delete the old release *before* the new one is live (that opens a window where `install.sh`/`install-cli.sh` resolve "latest" → 404). The flow:

1. **Merge to `main`** and wait for **CI** + **e2e** to go green on the merge commit (these build from source — no release needed).
2. **Tag today's date and push** (zero-padded `vYYYY.MM.DD`):
```bash
git tag -a v2026.05.03 -m "Orva v2026.05.03" && git push origin v2026.05.03
```
The Release workflow then **validates** (`go vet` + race tests) and *only on success* builds + publishes `ghcr.io/harsh-2002/orva:latest` (multi-arch), all CLI binaries, rootfs tarballs, checksums, and the GitHub Release. Nothing is built or published if validation fails (every build job `needs: validate`).
3. **On release publish**, `install-e2e` and `cli-e2e` run automatically against the freshly-published artifacts (they no longer trigger on main-push, which used to race the release cut).
4. **After** the new release is confirmed live, prune the previous one — last, not first:
```bash
gh release delete v<old-tag> --yes --cleanup-tag # removes the release + its tag
```

### Build-time identity

Expand Down
31 changes: 31 additions & 0 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,36 @@ choose_mode() {
log "mode: $MODE"
}

# ── Downloader bootstrap ─────────────────────────────────────────────────────
# Version resolution and asset downloads need curl (or wget). Minimal distro
# images (the install-e2e containers, and plenty of real bare-metal hosts) ship
# with neither, and the full prereq install happens later — so ensure a fetcher
# exists FIRST, installing curl via the detected package manager if necessary.
ensure_downloader() {
if have curl; then return; fi
[ "$DRYRUN" = "1" ] && return
log "curl not found — installing it"
case "$DISTRO_ID" in
ubuntu|debian)
DEBIAN_FRONTEND=noninteractive apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates curl ;;
alpine) apk add --no-cache ca-certificates curl ;;
fedora|rhel|centos|rocky|almalinux|amzn) { have dnf && dnf install -y curl; } || yum install -y curl ;;
arch|manjaro|endeavouros) pacman -Sy --noconfirm --needed curl ;;
opensuse-leap|opensuse-tumbleweed|sles) zypper --non-interactive install curl ;;
*)
case "$DISTRO_LIKE" in
*debian*|*ubuntu*) DEBIAN_FRONTEND=noninteractive apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates curl ;;
*rhel*|*fedora*) { have dnf && dnf install -y curl; } || yum install -y curl ;;
*arch*) pacman -Sy --noconfirm --needed curl ;;
*suse*) zypper --non-interactive install curl ;;
*) die "curl is required to download the release — install it and re-run" ;;
esac ;;
esac
have curl || die "failed to install curl"
}

# ── Version resolution ───────────────────────────────────────────────────────
resolve_version() {
if [ -n "$VERSION" ]; then log "version: $VERSION"; return; fi
Expand Down Expand Up @@ -1035,6 +1065,7 @@ main() {
decide_interactive
detect_existing
choose_mode
ensure_downloader
resolve_version

tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT INT TERM
Expand Down
Loading