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
14 changes: 11 additions & 3 deletions .github/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ including the PR title format and the Go checks that CI gates on, are in the roo
| `praxis.yaml` | PR to main/release-*, push to main, on `praxis/**` or the Go parser paths it ports | `cargo fmt --check`, clippy and tests for `praxis/`. A failure blocks only when `praxis/` or the workflow changed |
| `security-scans.yaml` | PR to main | Dependency review, shellcheck, YAML lint, Hadolint, Bandit, Trivy, CodeQL |
| `scorecard.yaml` | Weekly / push to main | OpenSSF Scorecard security health metrics |
| `praxis-latest.yaml` | Weekly (Monday), manual | Builds and tests `cortex-gateway` against the newest Praxis on crates.io. On failure it opens one issue, or comments on the open one. It blocks nothing |
| `release-binaries.yaml` | Tag push (`v*`), manual; push to main publishes the main-latest channel when `MAIN_CHANNEL_ENABLED` is set | Release tarballs — see [Release binaries](#release-binaries) |
| `pr-verifier.yml` | PR | PR title check |
| `spellcheck_action.yml` | PR | Spellcheck on markdown files — **disabled** (`if: false`) |
Expand Down Expand Up @@ -63,7 +64,14 @@ musl, in `golang:alpine` via `docker run`). There is no darwin/amd64 FFI. The
executable inside is still `cortex`, and `cortex --version` prints
`cortex (cpex) <ver>`. `install.sh --cpex` installs it.

The workflow's jobs are `version`, `build`, `build-cpex` (both `contents: read`) and
`publish`. Only `publish` writes. It refuses to publish unless every expected tarball
arrived, writes one `checksums.txt` over all of them, and attests every tarball's
The praxis-mode gateway ships as `cortex-gateway_<ver>_<os>_<arch>.tar.gz`, from the
`build-gateway` matrix job: darwin/arm64 natively, and linux/amd64 and linux/arm64 as
static musl binaries in `rust:alpine`. `scripts/build-gateway.sh` builds it and
`scripts/check-gateway-linkage.sh` checks that it is standalone. It is optional: a
failed `build-gateway` leg does not block the release.

The workflow's jobs are `version`, `build`, `build-cpex`, `build-gateway` (all
`contents: read`) and `publish`. Only `publish` writes. It refuses to publish unless
every expected tarball arrived, except the gateway tarballs: a missing one gives a
warning. It writes one `checksums.txt` over all of them, and attests every tarball's
build provenance.
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ updates:
directory: /praxis
schedule:
interval: weekly
# openssl-src is transitive (openssl, vendored), so the default skips it. Its bump
# is how cortex-gateway gets OpenSSL CVE fixes (praxis/README.md). With an allow
# list, only listed dependencies update, so direct must stay.
allow:
- dependency-type: direct
- dependency-name: openssl-src
dependency-type: indirect

# Python - root (tests)
- package-ecosystem: pip
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,18 @@ jobs:
- name: Run release_notes.sh tests
run: sh scripts/release_notes_test.sh

# check-gateway-linkage.sh runs only at release time, so without this job a
# regression in it would show only on a release.
gateway-linkage-script:
name: check-gateway-linkage.sh tests
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Run check-gateway-linkage.sh tests
run: sh scripts/check-gateway-linkage_test.sh

# `go mod tidy -diff` on every module in the repo. GOWORK=off so
# replace directives resolve as they do in build.yaml and
# release-binaries.yaml.
Expand Down
105 changes: 105 additions & 0 deletions .github/workflows/praxis-latest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Praxis latest - build cortex-gateway weekly against the newest Praxis release.
#
# Moves the praxis-proxy pins to the newest crates.io version, then runs the tests and
# the static release build. It blocks nothing. On failure, the report job comments on
# the open "Weekly build against the latest Praxis failed" issue, or opens it.
#
name: Praxis latest

on:
schedule:
- cron: "17 6 * * 1"
workflow_dispatch:

permissions:
contents: read

jobs:
build:
name: Build against the latest Praxis
runs-on: ubuntu-latest
timeout-minutes: 90
env:
# The same pinned image as build-gateway in release-binaries.yaml.
RUST_ALPINE: docker.io/library/rust:1.96.0-alpine@sha256:f87aa870663e2b57ec8c69de82c7eedf7383bee987eef7612c0359635eaadb41
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# crates.io refuses a request with no User-Agent.
- name: Resolve the newest Praxis version
id: praxis
run: |
set -euo pipefail
v=$(curl -fsSL -A "rossoctl-cortex-ci (github.com/rossoctl/cortex)" \
https://crates.io/api/v1/crates/praxis-proxy | jq -r .crate.max_stable_version)
if ! printf '%s\n' "$v" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::crates.io gave no usable praxis-proxy version: '$v'"
exit 1
fi
pinned=$(sed -n -E 's/^praxis-proxy = \{ version = "=([0-9.]+)".*/\1/p' praxis/server/Cargo.toml)
echo "Pinned Praxis: ${pinned}. Newest Praxis: ${v}."
echo "version=${v}" >> "$GITHUB_OUTPUT"

- name: Move the Praxis pins
env:
PRAXIS_VERSION: ${{ steps.praxis.outputs.version }}
run: |
set -euo pipefail
v="${PRAXIS_VERSION}"
sed -i -E 's/^(praxis-proxy(-filter)? = \{ version = )"=[0-9.]+"/\1"='"$v"'"/' \
praxis/server/Cargo.toml praxis/filters/Cargo.toml
git diff --stat
# If sed matched nothing, the job would test the old pin and pass.
for f in praxis/server/Cargo.toml praxis/filters/Cargo.toml; do
pins=$(grep -E '^praxis-proxy(-filter)? *=' "$f" || true)
if [ -z "$pins" ] || printf '%s\n' "$pins" | grep -vqF "\"=${v}\""; then
echo "::error::${f}: the Praxis pins did not move to =${v}"
exit 1
fi
done

# Installs the toolchain that praxis/rust-toolchain.toml names.
- name: Test
working-directory: praxis
run: |
set -euo pipefail
rustup toolchain install
# --recursive: a new Praxis can need newer shared dependencies too.
cargo update --recursive -p praxis-proxy -p praxis-proxy-filter
cargo test --locked

- name: Static release build
run: |
set -euo pipefail
docker run --rm -v "${GITHUB_WORKSPACE}:/src" -w /src -e VERSION=praxis-latest \
"${RUST_ALPINE}" sh scripts/build-gateway.sh

report:
name: Report a failure
needs: build
if: failure()
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
issues: write
env:
GH_TOKEN: ${{ github.token }}
TITLE: Weekly build against the latest Praxis failed
steps:
- name: Comment on or open the issue
run: |
set -euo pipefail
run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
n=$(gh issue list --repo "${GITHUB_REPOSITORY}" --state open \
--search "in:title \"${TITLE}\"" --json number --jq '.[0].number')
if [ -n "$n" ]; then
gh issue comment "$n" --repo "${GITHUB_REPOSITORY}" --body "Failed again: ${run_url}"
else
gh issue create --repo "${GITHUB_REPOSITORY}" --title "${TITLE}" \
--body "Failed run: ${run_url}

This job does not block anything. See the run log for the cause."
fi
105 changes: 99 additions & 6 deletions .github/workflows/release-binaries.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ name: Release binaries
# platform builds on its own runner. Linux builds are static musl binaries, so
# one tarball runs on glibc and musl hosts alike.
#
# Four jobs: `version` derives the tag and the stamp once, `build` and
# `build-cpex` hold only contents: read, and `publish` alone can write. It
# publishes nothing unless every expected tarball arrived, so a failed cpex
# build blocks the release rather than shipping without the variant (and the
# main-latest prune below never deletes the variant's assets).
# `cortex-gateway` is the praxis-mode gateway, a Rust binary. scripts/build-gateway.sh
# builds it on each platform's runner (static musl in rust:alpine on Linux) and
# checks that it needs no library outside the OS.
#
# Five jobs: `version` derives the tag and the stamp once, `build`,
# `build-cpex` and `build-gateway` hold only contents: read, and `publish` alone
# can write. It publishes nothing unless every `build` and `build-cpex` tarball
# arrived, so a failed cpex build blocks the release rather than shipping without
# the variant (and the main-latest prune below never deletes the variant's assets).
# `build-gateway` is optional: a failed gateway leg does not block the release, and
# on main-latest the prune then removes the old gateway asset for that platform.
# That is correct: checksums.txt would not list it, so nothing could verify it.

on:
push:
Expand Down Expand Up @@ -297,8 +304,83 @@ jobs:
if-no-files-found: error
retention-days: 1

build-gateway:
name: build-gateway (${{ matrix.os }}/${{ matrix.arch }})
needs: version
runs-on: ${{ matrix.runner }}
permissions:
contents: read
timeout-minutes: 60
# A failed leg must not fail the run: release-smoke-linux.yaml runs only after a
# successful run, so the release would ship with no smoke test.
continue-on-error: true
strategy:
# Each platform is optional on its own: install.sh --praxis checks per os/arch
# in checksums.txt, so two of three tarballs is a valid release.
fail-fast: false
matrix:
include:
- {os: darwin, arch: arm64, runner: macos-15}
- {os: linux, arch: amd64, runner: ubuntu-latest}
- {os: linux, arch: arm64, runner: ubuntu-24.04-arm}
env:
TAG: ${{ needs.version.outputs.tag }}
VERSION: ${{ needs.version.outputs.version }}
TARGET_OS: ${{ matrix.os }}
TARGET_ARCH: ${{ matrix.arch }}
# Run with `docker run`, not as a job container, for the reason ALPINE_GO gives.
RUST_ALPINE: docker.io/library/rust:1.96.0-alpine@sha256:f87aa870663e2b57ec8c69de82c7eedf7383bee987eef7612c0359635eaadb41
# Clean hosts for the built binary: musl with no build tools, and no libc and
# no OpenSSL at all. Plain alpine ships libssl3, so it is not the second one.
ALPINE: docker.io/library/alpine:3.22@sha256:49340b346798875b5592e5d420241f2f9cfba5f47e62ebb1446a1b34734e898f
DISTROLESS: gcr.io/distroless/static-debian12@sha256:d75cdd72874d4790092fcb1b058493ecf6bb5bf2b2b897045b00ff01d91843f2
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Build cortex-gateway
run: |
set -euo pipefail
# The runner user makes dist/, so it can delete the binary root writes there.
mkdir -p dist
if [ "${TARGET_OS}" = linux ]; then
docker run --rm -v "${GITHUB_WORKSPACE}:/src" -w /src -e VERSION \
"${RUST_ALPINE}" sh scripts/build-gateway.sh
else
sh scripts/build-gateway.sh
fi

- name: Run cortex-gateway on clean hosts
if: matrix.os == 'linux'
run: |
set -euo pipefail
# glibc host, musl host, and a host with no libc and no OpenSSL.
dist/cortex-gateway --version
docker run --rm -v "${GITHUB_WORKSPACE}/dist:/d:ro" "${ALPINE}" /d/cortex-gateway --version
docker run --rm -v "${GITHUB_WORKSPACE}/dist:/d:ro" "${DISTROLESS}" /d/cortex-gateway --version

- name: Package cortex-gateway
run: |
set -euo pipefail
tar -C dist -czf "dist/cortex-gateway_${TAG}_${TARGET_OS}_${TARGET_ARCH}.tar.gz" cortex-gateway

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The gateway tarball changes the install statistics. scripts/collect-install-stats.sh platform_of puts every asset name that contains linux_amd64 (and the other platforms) into a platform bucket, so cortex-gateway_<tag>_linux_amd64.tar.gz counts as a linux tarball. docs/install-stats/index.html then halves the platform sum with perInstall = 2 (cortex + agentop). An install.sh --praxis install downloads three tarballs of one platform, so each praxis install counts as 1.5 in the Linux/Mac columns, and the CI subtraction on Linux mixes the two scales. It is not a blocker. A follow-up could give cortex-gateway_* its own bucket in platform_of (checked before the platform tests), or at least note this next to perInstall.

rm -f dist/cortex-gateway
ls -l dist

- name: Upload the tarball for the publish job
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: dist-gateway-${{ matrix.os }}-${{ matrix.arch }}
path: dist/*.tar.gz
if-no-files-found: error
retention-days: 1

publish:
needs: [version, build, build-cpex]
needs: [version, build, build-cpex, build-gateway]
# Run whatever build-gateway did, but only when build and build-cpex succeeded.
# !cancelled() is needed: without a status function, a failed need skips this job.
if: ${{ !cancelled() && needs.build.result == 'success' && needs.build-cpex.result == 'success' }}
runs-on: ubuntu-latest
permissions:
# create/update the Release, upload assets, and move the rolling tag.
Expand Down Expand Up @@ -353,6 +435,11 @@ jobs:
echo "::error::Missing release tarballs:${missing}"
exit 1
fi
# cortex-gateway is optional: warn, do not add it to missing.
for target in darwin/arm64 linux/amd64 linux/arm64; do
f="dist/cortex-gateway_${TAG}_${target%/*}_${target#*/}.tar.gz"
[ -s "${f}" ] || echo "::warning::No cortex-gateway tarball for ${target}. This release ships without it."
done
( cd dist && sha256sum ./*.tar.gz > checksums.txt )
ls -l dist
cat dist/checksums.txt
Expand Down Expand Up @@ -412,6 +499,12 @@ jobs:
add 'Prebuilt `agentop` and `cortex` binaries for linux and macOS (amd64/arm64).'
add '`cortex-cpex_*` is `cortex` built with the cpex policy plugin, for macOS arm64 and'
add 'linux amd64/arm64 (static). Install it with `install.sh --cpex`.'
# Name the gateway only when this release has at least one gateway tarball.
# No platform list: a failed leg leaves a platform out, and the assets show which.
if ls dist/cortex-gateway_*.tar.gz >/dev/null 2>&1; then
add '`cortex-gateway_*` is the praxis-mode gateway (static on linux).'
add 'Install it with `install.sh --praxis`.'
fi
add 'Every tarball carries build provenance:'
add '`gh attestation verify <tarball> --repo rossoctl/cortex`.'
add ''
Expand Down
Loading
Loading