From c8cf17556f447b24a557a7f7b42a60c8db0bcabe Mon Sep 17 00:00:00 2001 From: Harsh-2002 Date: Tue, 2 Jun 2026 10:00:21 +0000 Subject: [PATCH] ci(release): validate before release; run install/cli-e2e on release publish; fix install.sh curl bootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release pipeline released first and validated in parallel — backwards. Fix the ordering and the coupling: - release.yml: add a `validate` gate (go vet + race tests over the full module); every build job now `needs: validate`, so nothing is built or published unless tests pass. Release is genuinely the last step. - install-e2e.yml / cli-e2e.yml: these download the RELEASED binary, so move them off main-push (which raced the release cut — the old release was already deleted and the new one not yet published, so "latest" 404'd) onto `release: published`. They now validate the freshly-published artifacts. macOS/Windows CLI install jobs also run on release. - install.sh: a pre-existing bug (since the 2026-05-30 installer rewrite) — it calls curl to resolve the release BEFORE installing prerequisites, so minimal hosts/containers without curl failed with "curl: not found". Add ensure_downloader() to install curl up front (verified end-to-end via test/install/run-distro.sh debian12 against the published release). - CLAUDE.md: rewrite the release policy — validate first, release last, prune the OLD release AFTER the new one is live (never before). --- .github/workflows/cli-e2e.yml | 19 ++++++++---------- .github/workflows/install-e2e.yml | 17 ++++++---------- .github/workflows/release.yml | 33 +++++++++++++++++++++++++++++++ CLAUDE.md | 24 +++++++++++----------- scripts/install.sh | 31 +++++++++++++++++++++++++++++ 5 files changed, 91 insertions(+), 33 deletions(-) diff --git a/.github/workflows/cli-e2e.yml b/.github/workflows/cli-e2e.yml index 1587382..5d2bc52 100644 --- a/.github/workflows/cli-e2e.yml +++ b/.github/workflows/cli-e2e.yml @@ -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/**" @@ -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: @@ -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: diff --git a/.github/workflows/install-e2e.yml b/.github/workflows/install-e2e.yml index a8f7fd7..821c338 100644 --- a/.github/workflows/install-e2e.yml +++ b/.github/workflows/install-e2e.yml @@ -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" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 06bec09..a6607a0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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: @@ -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: @@ -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: @@ -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 @@ -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: diff --git a/CLAUDE.md b/CLAUDE.md index 1e9cdf1..5f0ce1c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 --yes -git tag -d v && git push origin --delete v -git tag v2026.05.03 && git push origin v2026.05.03 -``` - -The release workflow builds `ghcr.io/harsh-2002/orva:` + `: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 --yes --cleanup-tag # removes the release + its tag + ``` ### Build-time identity diff --git a/scripts/install.sh b/scripts/install.sh index ea25054..017f5b3 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -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 @@ -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