From 4cb7e7ff37609c8914433a742463e4182cf123ae Mon Sep 17 00:00:00 2001 From: andyne13 Date: Thu, 30 Jul 2026 22:05:40 +0200 Subject: [PATCH 1/2] chore(release): bump version to 2.1.0 --- infra/charts/openrag-stack/Chart.yaml | 4 ++-- infra/charts/openrag-stack/values.yaml | 6 +++--- infra/compose/docker-compose.yaml | 4 ++-- pyproject.toml | 2 +- uv.lock | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/infra/charts/openrag-stack/Chart.yaml b/infra/charts/openrag-stack/Chart.yaml index 08591a260..557bbbd0a 100644 --- a/infra/charts/openrag-stack/Chart.yaml +++ b/infra/charts/openrag-stack/Chart.yaml @@ -3,8 +3,8 @@ name: openrag-stack description: A Helm chart for Kubernetes type: application -version: 0.6.0 -appVersion: "2.0.1" +version: 0.6.1 +appVersion: "2.1.0" maintainers: - name: linagora diff --git a/infra/charts/openrag-stack/values.yaml b/infra/charts/openrag-stack/values.yaml index cc477bdb0..23e52e495 100644 --- a/infra/charts/openrag-stack/values.yaml +++ b/infra/charts/openrag-stack/values.yaml @@ -99,7 +99,7 @@ ray: registry: "ghcr.io" repository: "linagora/openrag-ray" # Pin to a release tag (ideally a digest) for reproducible deploys. - tag: "v2.0.1" + tag: "v2.1.0" resources: head: requests: @@ -383,7 +383,7 @@ adminUi: repository: "linagoraai/openrag-admin-ui" # Pin to a release tag (ideally a digest) for reproducible deploys. Must be a # build from infra/docker/ui.Dockerfile (nginx-unprivileged, listens :8080). - tag: "v2.0.1" + tag: "v2.1.0" pullPolicy: IfNotPresent replicaCount: 1 service: @@ -440,7 +440,7 @@ openrag: registry: "" repository: "linagoraai/openrag" # Pin to a release tag (ideally a digest) for reproducible deploys. - tag: "v2.0.1" + tag: "v2.1.0" pullPolicy: IfNotPresent service: type: ClusterIP diff --git a/infra/compose/docker-compose.yaml b/infra/compose/docker-compose.yaml index 5ca821fb5..38d1bb3a4 100644 --- a/infra/compose/docker-compose.yaml +++ b/infra/compose/docker-compose.yaml @@ -18,7 +18,7 @@ x-openrag-env: &openrag_env FONT_PATH: ${FONT_PATH:-/app/data/fonts/GoNotoCurrent-Regular.ttf} x-openrag: &openrag_template - image: linagoraai/openrag:v2.0.1 + image: linagoraai/openrag:v2.1.0 # Start as root so entrypoint.sh can grant GID-0 write on the bind-mounted # writable dirs (data/, logs/, the HF cache) — which a non-root container # can't write when Docker auto-creates them root-owned — then it immediately @@ -113,7 +113,7 @@ x-vllm: &vllm_template services: # ── Admin UI (React SPA + nginx, same-origin reverse proxy to the API) ── admin-ui: - image: linagoraai/openrag-admin-ui:v2.0.1 + image: linagoraai/openrag-admin-ui:v2.1.0 build: context: ../.. dockerfile: infra/docker/ui.Dockerfile diff --git a/pyproject.toml b/pyproject.toml index 21e5f5b89..a312607a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "openrag" -version = "2.0.1" +version = "2.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.12" diff --git a/uv.lock b/uv.lock index 06d2a8b73..5dfa8b8f1 100644 --- a/uv.lock +++ b/uv.lock @@ -2713,7 +2713,7 @@ wheels = [ [[package]] name = "openrag" -version = "2.0.1" +version = "2.1.0" source = { editable = "." } dependencies = [ { name = "aiobreaker" }, From 184153adfe7e6305d57427af9b962cb79570faf5 Mon Sep 17 00:00:00 2001 From: andyne13 Date: Thu, 30 Jul 2026 22:09:11 +0200 Subject: [PATCH 2/2] docs(release): add a post-release check that the GA images really published The v2.0.1 release produced a green build.yml run that built nothing: the build jobs were guarded on github.event.base_ref, which is empty for a tag pushed to a branch-protected main, so all three reported skipped while the run stayed green. The guard is fixed, but nothing in our process would have caught it, and the same class of failure can recur silently. This records the checks that prove a release shipped: assert per-job conclusions rather than the run's, resolve the manifest digest for all five registry coordinates, confirm latest matches the release and that ghcr and Docker Hub carry the same build, and read the version baked into the image so a tag placed on a pre-bump commit is caught. Includes the reconstructed v2.0.1 sequence and the rules it produced, plus the open risk that main's hardened build.yml has never yet run on a GA tag. --- .github/RELEASING.md | 236 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 .github/RELEASING.md diff --git a/.github/RELEASING.md b/.github/RELEASING.md new file mode 100644 index 000000000..b020a8a0c --- /dev/null +++ b/.github/RELEASING.md @@ -0,0 +1,236 @@ +# Releasing OpenRag — verifying the GA images actually published + +Run this **after** pushing a `vX.Y.Z` tag to `main`. It exists because the +v2.0.1 release produced a **green workflow run that built nothing**: the three +build jobs were guarded on `github.event.base_ref`, which is empty for a tag +pushed to a branch-protected `main`, so every job reported `skipped` and the run +was still green. Nothing in our process caught it. + +The rule this checklist enforces: **a green run is not proof. A digest in the +registry is proof.** + +Set the version once, and define the digest helper every step below uses: + +```bash +export VER=v2.1.0 # the tag you just pushed +dg() { docker buildx imagetools inspect --raw "$1" 2>/dev/null | sha256sum | awk '{print "sha256:"$1}'; } +``` + +> **Why `--raw | sha256sum` and not `--format '{{.Manifest.Digest}}'`:** buildx +> v0.30.1 **silently ignores** that `--format` template and prints its default +> human output instead. Comparing those strings makes every image look like it +> drifted. A manifest digest *is* the sha256 of the raw manifest bytes, so this +> form is both correct and self-verifying. Validated against `v2.0.1`, where it +> reproduces the published digest exactly. + +## What a release publishes + +| Image | ghcr.io | Docker Hub | +|---|---|---| +| API | `ghcr.io/linagora/openrag` | `linagoraai/openrag` | +| Ray | `ghcr.io/linagora/openrag-ray` | — (ghcr only, by design) | +| Admin UI | `ghcr.io/linagora/openrag-admin-ui` | `linagoraai/openrag-admin-ui` | + +Each gets two tags: `$VER` and `latest`. + +--- + +## 1. The workflow ran — and did not skip + +A skipped job is the exact v2.0.1 failure mode, and `gh run list` shows such a +run as `success`. Assert on **per-job conclusions**, never on the run's. + +```bash +gh run list --workflow build.yml --limit 5 \ + --json databaseId,headBranch,event,status,conclusion,createdAt \ + --jq '.[] | "\(.databaseId) \(.event) \(.headBranch) \(.status)/\(.conclusion) \(.createdAt)"' +``` + +Take the run id for the tag push, then: + +```bash +RUN_ID= +gh run view "$RUN_ID" --json jobs \ + --jq '.jobs[] | "\(.conclusion)\t\(.name)"' +``` + +**PASS** requires all four jobs `success`: +`verify-tag`, `build-and-push-image`, `build-and-push-image-ray`, +`build-and-push-image-admin-ui`. + +**FAIL** on any `skipped` — that is the v2.0.1 bug recurring. A hard gate: + +```bash +gh run view "$RUN_ID" --json jobs --jq '[.jobs[] | select(.conclusion != "success")] | length' +# must print 0 +``` + +If `verify-tag` failed loudly, the tag is not an ancestor of `origin/main` — +fix the tag placement, do not rerun. + +## 2. The tag exists in every registry + +`imagetools inspect` reads the registry directly (anonymous, no pull, no +`docker login`). If the tag was never pushed, this errors — which is the point. + +```bash +for img in ghcr.io/linagora/openrag ghcr.io/linagora/openrag-ray \ + ghcr.io/linagora/openrag-admin-ui \ + linagoraai/openrag linagoraai/openrag-admin-ui; do + d=$(dg "$img:$VER"); printf '%-42s %s\n' "$img:$VER" "${d:-MISSING}" +done +``` + +**PASS**: five `sha256:…` digests, zero `MISSING`. + +## 3. `latest` points at the release, not something older + +`latest` is published unconditionally by `build.yml`, so a mismatch here means +`latest` is stale and every `docker pull` without a tag gets the wrong build. + +```bash +for img in ghcr.io/linagora/openrag ghcr.io/linagora/openrag-ray \ + ghcr.io/linagora/openrag-admin-ui \ + linagoraai/openrag linagoraai/openrag-admin-ui; do + v=$(dg "$img:$VER"); l=$(dg "$img:latest") + if [ -n "$v" ] && [ "$v" = "$l" ]; then echo "OK $img" + else echo "DRIFT $img"; echo " $VER = ${v:-none}"; echo " latest= ${l:-none}"; fi +done +``` + +**PASS**: all `OK`. + +## 4. ghcr and Docker Hub are the same build + +Both registries are pushed from one `docker/build-push-action` step, so the +digests must be identical. A difference means one push failed and was +back-filled from a different build. + +```bash +for pair in "ghcr.io/linagora/openrag linagoraai/openrag" \ + "ghcr.io/linagora/openrag-admin-ui linagoraai/openrag-admin-ui"; do + set -- $pair; a=$(dg "$1:$VER"); b=$(dg "$2:$VER") + [ "$a" = "$b" ] && echo "OK $1 == $2" || echo "MISMATCH $1=$a $2=$b" +done +``` + +**PASS**: both `OK`. + +## 5. Pull it, and confirm the digest is the one you verified + +Steps 2–4 read metadata. This proves the bytes are actually fetchable. + +```bash +docker pull "linagoraai/openrag:$VER" +docker image inspect "linagoraai/openrag:$VER" --format '{{index .RepoDigests 0}}' +``` + +**PASS**: the printed digest equals the Docker Hub digest from step 2. + +## 6. The image contains the released code + +The strongest check, and the one that catches a build from the wrong commit: +the version baked into the image must equal the tag. `app.version` comes from +`importlib.metadata`, i.e. from `pyproject.toml` at build time. + +```bash +docker run --rm --entrypoint grep "linagoraai/openrag:$VER" -m1 '^version' /app/pyproject.toml +# expect: version = "2.1.0" (tag minus the leading v) +``` + +And on a running stack (the endpoint is unauthenticated): + +```bash +curl -fsS http://:8080/version +# {"version":"2.1.0"} +``` + +**PASS**: both report the release version. A mismatch means the tag sat on a +commit that predates the version bump — the images are mislabelled and must be +rebuilt from a corrected tag. + +## 7. The tag is where it should be + +```bash +git fetch origin main --tags +git tag --contains "$VER" >/dev/null 2>&1 +git merge-base --is-ancestor "$VER" origin/main && echo "OK: $VER is on main" || echo "FAIL: not on main" +git log -1 --format='%H %s' "$VER" +``` + +**PASS**: `OK`, and the commit is the release-branch merge commit. + +## 8. Chart and compose reference the published tags + +The chart and compose pins are part of the release surface; shipping them +pointing at the previous version is a silent regression for anyone deploying +from the tag. + +```bash +git show "$VER:infra/charts/openrag-stack/Chart.yaml" | grep -E '^(version|appVersion)' +git show "$VER:infra/charts/openrag-stack/values.yaml" | grep -nE 'tag: "v[0-9]' +git show "$VER:infra/compose/docker-compose.yaml" | grep -nE 'image: linagoraai/' +``` + +**PASS**: every OpenRag image pin reads `$VER`, `appVersion` matches, chart +`version` was bumped. + +--- + +## What v2.0.1 actually did, and the rules that follow + +Reconstructed from the run log and the tag, 2026-07-30. Three `build.yml` runs +fired for the same tag name: + +| run | time (UTC) | commit | result | +|---|---|---|---| +| 30034799802 | 18:41 | `c08c5e9f` (release/2.0.1 → main merge) | 3 jobs **skipped**, run green | +| 30035356446 | 18:49 | `c08c5e9f` — tag re-pushed unchanged | 3 jobs **skipped** again | +| 30039985149 | 19:55 | `6a18a534` (CI hotfix #764 merge) | `verify-tag` + 3 builds **success** | + +The shipped `v2.0.1` tag therefore sits on the **CI-hotfix merge commit**, not +on the release-branch merge. The tagged tree still carries the version bump +(`c08c5e9f` is its ancestor), so the images are correct — confirmed above. + +Rules this produces: + +1. **A skipped job is a failure.** The first two runs reported `success` at the + run level. Only per-job conclusions revealed the truth. That is step 1. +2. **Never re-push a tag to "retry".** Run 2 proves it is deterministic: the + workflow that executes is the one *at the tagged commit*, so re-pushing the + same tag re-runs the same broken file. Move the tag to a fixed commit, or + fix nothing and diagnose. +3. **Verify images before announcing.** `build.yml` does not create GitHub + Releases. For v2.0.1 the Release was published at 20:01, six minutes after + the images finally landed at 19:55. Keep that order: tag → images verified + → Release notes. +4. **Dry-run the verification itself against the previous release.** Doing that + for v2.0.1 is what exposed the broken `--format` flag above. A checklist + that silently reports nonsense is worse than none. + +### Open risk for the next release + +`main`'s current `build.yml` is **not** the file that successfully built +v2.0.1. PR #767 hardened it afterwards (exact-tag regex, tag name/SHA passed as +env instead of `${{ }}` interpolation, `persist-credentials: false`). The next +GA tag is the **first time that hardened guard ever runs**. + +Pre-flighted locally on 2026-07-30: + +- Regex `^v[0-9]+\.[0-9]+\.[0-9]+$` — `v2.1.0` accepted; `v2.1`, `2.1.0`, + `v1.2.3-rc1`, `v1.0-hardening` rejected loudly; `v2.1.0-rc.N` filtered out by + the job-level `if` and left to `build_rc.yml`. Correct on all six. +- `persist-credentials: false` + `git fetch --no-tags origin main` — verified an + anonymous fetch of this repo succeeds, so the guard can still reach `main`. + This holds only while the repo is **public**; if it is ever made private, + that fetch breaks and every GA build blocks. + +The residual risk is acceptable because the hardened guard's failure mode is +`exit 1` — loud and blocking — not v2.0.1's silent skip. But treat step 1 as +mandatory, not a formality. + +## Result + +Record the outcome on the GitHub Release or the milestone. If any step fails, +the release is **not** done — publishing images is the deliverable, and the tag +alone delivers nothing.