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
36 changes: 36 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,42 @@ jobs:
kubeconform -schema-location '/tmp/app-schema.json' \
-summary examples/static-inline-app.yaml examples/static-git-app.yaml

versions:
name: Version strings
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# release-please bumps all of these together; a hand edit to one of them
# would ship mismatched versions.
- name: Every version string matches .release-please-manifest.json
run: |
python3 - <<'PY'
import json, re, sys
want = json.load(open(".release-please-manifest.json"))["."]
def grep(path, pattern):
m = re.search(pattern, open(path).read(), re.M)
return m.group(1) if m else None
lock = json.load(open("ui/package-lock.json"))
found = {
"Chart.yaml version": grep("charts/nebari-apps/Chart.yaml", r"^version:\s*\"?([^\"\s#]+)"),
"Chart.yaml appVersion": grep("charts/nebari-apps/Chart.yaml", r"^appVersion:\s*\"?([^\"\s#]+)"),
"api __version__": grep("api/src/nebari_apps_api/__init__.py", r"^__version__ = \"([^\"]+)\""),
"mcp __version__": grep("mcp/src/nebari_apps_mcp/__init__.py", r"^__version__ = \"([^\"]+)\""),
"ui/package.json": json.load(open("ui/package.json"))["version"],
"ui/package-lock.json": lock["version"],
"ui/package-lock.json packages['']": lock["packages"][""]["version"],
}
bad = {k: v for k, v in found.items() if v != want}
for k, v in found.items():
print(("ok " if v == want else "FAIL ") + f"{k}: {v}")
if bad:
print(f"::error::version strings must all be {want} (the release-please manifest)")
sys.exit(1)
PY

pins:
name: Runtime pins
runs-on: ubuntu-latest
Expand Down
42 changes: 42 additions & 0 deletions .github/workflows/release-please.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Release PR

# Keeps one open "chore: release X.Y.Z" pull request that bumps every version
# string (Chart.yaml version and appVersion, the api and mcp __version__,
# ui/package.json and its lockfile) and prepends the release's CHANGELOG.md
# section, built from the conventional-commit titles merged since the last
# release. Merging it is the release: the Chart.yaml bump runs release.yaml.
#
# release-please never creates the tag or GitHub Release itself
# (skip-github-release): pack-release does, as nebari-apps-<version>, and the
# tag format below matches it so release-please finds the previous release.
# Labels are skipped too, so a merged release PR is never left "pending".

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
release-pr:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
with:
# A PR opened with the default GITHUB_TOKEN does not trigger CI; set
# RELEASE_PLEASE_TOKEN (a bot token with contents + pull-requests
# write) so the release PR runs its checks without a manual reopen.
token: ${{ secrets.RELEASE_PLEASE_TOKEN || github.token }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
skip-github-release: true
skip-labeling: true
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.1.0"
}
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Changelog

Maintained by [release-please](https://github.com/googleapis/release-please) from the
conventional-commit titles of merged pull requests. See [Releases](README.md#releases).

## 0.1.0 (2026-08-26)

First release: the App CRD and apps-operator, apps-api, apps-ui and apps-mcp, and the
`nebari-apps` Helm chart. See the [v0.1.0 release](https://github.com/nebari-dev/apps-pack/releases/tag/v0.1.0).
28 changes: 24 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,14 @@ Alpha means the happy path works; these are the gaps to know about before a demo
# App namespaces first: the chart grants the API a RoleBinding in each one
# listed in api.allowedNamespaces (default: apps).
kubectl create namespace apps && kubectl label namespace apps nebari.dev/managed=true
# From a checkout: the chart's default image tags are its appVersion, which
# only matches images at a release commit. On main, use the images main builds.
helm install nebari-apps charts/nebari-apps \
--namespace nebari-apps --create-namespace \
--set clusterDomain=<your-cluster-domain> \
--set keycloak.url=https://keycloak.<your-cluster-domain>/auth
--set keycloak.url=https://keycloak.<your-cluster-domain>/auth \
--set operator.image.tag=latest --set api.image.tag=latest \
--set ui.image.tag=latest --set mcp.image.tag=latest
kubectl label namespace nebari-apps nebari.dev/managed=true
```

Expand Down Expand Up @@ -286,9 +290,21 @@ with cosign, with a build-provenance attestation and an SBOM. The same push also
pins exact `sha-` tags. Pull requests build the images without pushing, and every image is
scanned with Trivy; a HIGH or CRITICAL vulnerability with an available fix fails the check.

**Releasing is a chart version bump.** Merging a change to `version` in
[`charts/nebari-apps/Chart.yaml`](charts/nebari-apps/Chart.yaml) runs
[`release.yaml`](.github/workflows/release.yaml), which uses the org's `pack-release`
**Releasing is merging the release PR.**
[`release-please.yaml`](.github/workflows/release-please.yaml) keeps one open
`chore: release X.Y.Z` pull request, opened by
[release-please](https://github.com/googleapis/release-please). It bumps every version string
together (Chart `version` and `appVersion`, the api and mcp `__version__`, `ui/package.json`
and its lockfile) and adds the release's section to [`CHANGELOG.md`](CHANGELOG.md), written
from the conventional-commit titles of the PRs merged since the last release (`feat:` → minor
while we are below 1.0, `fix:` → patch). A CI check fails if those strings ever disagree with
[`.release-please-manifest.json`](.release-please-manifest.json). Without a
`RELEASE_PLEASE_TOKEN` secret, the release PR is opened with `GITHUB_TOKEN` and its checks do
not start until a maintainer closes and reopens it.

Merging it changes `version` in
[`charts/nebari-apps/Chart.yaml`](charts/nebari-apps/Chart.yaml), which runs
[`release.yaml`](.github/workflows/release.yaml). That uses the org's `pack-release`
workflow to:

- pin the chart's four image tags to `sha-<release commit>` (those images are built by the
Expand All @@ -308,6 +324,10 @@ helm install nebari-apps oci://ghcr.io/nebari-dev/apps-pack/charts/nebari-apps \
--set keycloak.url=https://keycloak.<your-cluster-domain>/auth
```

A running install reports its version and image build at `GET /api/v1/version`
(`{"version": "0.2.0", "build": "sha-abc1234"}`), and the UI shows it at the bottom of the
account menu. Include it in bug reports.

To check an image's or chart's signature, see
[Verifying Nebari pack artifacts](https://github.com/nebari-dev/.github/blob/main/verifying-nebari-artifacts.md).
The signer identity is the org workflow at the commit this repo pins
Expand Down
7 changes: 6 additions & 1 deletion api/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "nebari-apps-api"
version = "0.1.0"
dynamic = ["version"]
description = "REST API for the Nebari Apps Pack - CRUD + observability over App custom resources"
requires-python = ">=3.12"
dependencies = [
Expand All @@ -26,6 +26,11 @@ dev = [
requires = ["hatchling"]
build-backend = "hatchling.build"

# The version lives in __init__.py (release-please bumps it), so uv.lock
# does not record it and a release needs no relock.
[tool.hatch.version]
path = "src/nebari_apps_api/__init__.py"

[tool.hatch.build.targets.wheel]
packages = ["src/nebari_apps_api"]

Expand Down
2 changes: 1 addition & 1 deletion api/src/nebari_apps_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""nebari-apps-api - REST API for the Nebari Apps Pack."""

__version__ = "0.1.0"
__version__ = "0.1.0" # x-release-please-version
4 changes: 4 additions & 0 deletions api/src/nebari_apps_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ class Settings:
ui_keycloak_realm: str = field(default_factory=lambda: os.environ.get("UI_KEYCLOAK_REALM", "nebari"))
ui_keycloak_client_id: str = field(default_factory=lambda: os.environ.get("UI_KEYCLOAK_CLIENT_ID", ""))

# The image this API runs from (the chart passes its image tag, sha-<commit>
# for released charts), reported next to the version by /api/v1/version.
build: str = field(default_factory=lambda: os.environ.get("APPS_BUILD", "unknown"))

# Serve apps over plain HTTP (mirrors the operator's --tls-disabled).
tls_disabled: bool = field(default_factory=lambda: _env_bool("TLS_DISABLED", False))

Expand Down
10 changes: 9 additions & 1 deletion api/src/nebari_apps_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from fastapi import Depends, FastAPI, File, Form, HTTPException, Query, UploadFile
from fastapi.responses import JSONResponse

from . import __version__
from . import cr as crmod
from .auth import User, can_access, current_user, is_admin
from .config import settings
Expand Down Expand Up @@ -45,7 +46,7 @@


def create_app(store: AppStore | None = None) -> FastAPI:
app = FastAPI(title="nebari-apps-api", version="0.1.0")
app = FastAPI(title="nebari-apps-api", version=__version__)
if not settings.auth_enabled:
log.warning(
"AUTHENTICATION IS DISABLED (AUTH_ENABLED=false): every request is anonymous and "
Expand Down Expand Up @@ -189,10 +190,17 @@ def readyz(store: Store) -> JSONResponse:
ready = all(v == "ok" for v in checks.values())
return JSONResponse({"status": "ok" if ready else "unavailable", "checks": checks}, 200 if ready else 503)

@app.get(PREFIX + "/version")
def version() -> dict[str, str]:
"""Public: which release and image this API is, for bug reports."""
return {"version": __version__, "build": settings.build}

@app.get(PREFIX + "/config")
def ui_config() -> dict[str, Any]:
"""Public bootstrap config for the UI (keycloak-js)."""
return {
"version": __version__,
"build": settings.build,
"authEnabled": settings.auth_enabled,
"keycloak": {
"url": settings.ui_keycloak_url,
Expand Down
10 changes: 10 additions & 0 deletions api/tests/test_apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,16 @@ def test_catalogs(client):
assert config["authEnabled"] is False


def test_version(client, monkeypatch):
from nebari_apps_api import __version__
from nebari_apps_api.config import settings

monkeypatch.setattr(settings, "build", "sha-abc1234")
assert client.get("/api/v1/version").json() == {"version": __version__, "build": "sha-abc1234"}
config = client.get("/api/v1/config").json()
assert (config["version"], config["build"]) == (__version__, "sha-abc1234")


def test_analytics_summary(client):
client.post("/api/v1/apps", json=make_app_body())
body = make_app_body(name="two", namespace="team-a")
Expand Down
1 change: 0 additions & 1 deletion api/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions charts/nebari-apps/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ description: >-
apps-operator that reconciles Apps into Deployments, Services, and
NebariApps.
type: application
version: 0.1.0
appVersion: "0.1.0"
version: 0.1.0 # x-release-please-version
appVersion: "0.1.0" # x-release-please-version
home: https://github.com/nebari-dev/apps-pack
sources:
- https://github.com/nebari-dev/apps-pack
2 changes: 2 additions & 0 deletions charts/nebari-apps/templates/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ spec:
value: {{ .Values.api.auth.enabled | quote }}
- name: TLS_DISABLED
value: {{ not .Values.tls.enabled | quote }}
- name: APPS_BUILD
value: {{ .Values.api.image.tag | default .Chart.AppVersion | quote }}
{{- if .Values.api.auth.enabled }}
- name: OIDC_ISSUER
value: {{ printf "%s/realms/%s" (required "keycloak.url is required when api.auth.enabled" .Values.keycloak.url | trimSuffix "/") .Values.keycloak.realm }}
Expand Down
5 changes: 3 additions & 2 deletions docs/src/content/docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ both thin clients over this API.

## Authentication

All endpoints except `/healthz`, `/readyz` and `/config` require a Keycloak **JWT bearer token**:
All endpoints except `/healthz`, `/readyz`, `/version` and `/config` require a Keycloak **JWT bearer token**:

```
Authorization: Bearer <access-token>
Expand Down Expand Up @@ -44,7 +44,8 @@ create.
|---|---|
| `GET /healthz` | Liveness (public). Always `200` while the process serves. |
| `GET /readyz` | Readiness (public): `200 {"status": "ok", "checks": {...}}` when the Kubernetes API answers a namespace list and, with auth on, the realm JWKS loads (each within 3s; a good JWKS fetch is reused for 60s). Otherwise `503`, with the failing check marked `"failed"` and the reason in the API's log. |
| `GET /config` | UI bootstrap config (public): `authEnabled`, Keycloak url/realm/SPA client id, `appsDomain`, `appsScheme`. |
| `GET /version` | Release and image build (public): `{"version": "0.2.0", "build": "sha-abc1234"}`. `build` is the chart's `api.image.tag` (`unknown` outside the chart). |
| `GET /config` | UI bootstrap config (public): `authEnabled`, Keycloak url/realm/SPA client id, `appsDomain`, `appsScheme`, and the `version`/`build` above. |
| `GET /capabilities` | `{appsDomain, sourceTypes, namespaces}` — what this cluster supports and where the caller may launch. `sourceTypes` is `["git", "inline", "pvc"]`. |
| `GET /auth/me` | The caller's username, email, groups, and `admin` (member of an admin group). |

Expand Down
6 changes: 5 additions & 1 deletion docs/src/content/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,14 @@ kubectl label namespace apps nebari.dev/managed=true
Then install:

```bash
# From a checkout: the chart's default image tags are its appVersion, which
# only matches images at a release commit. On main, use the images main builds.
helm install nebari-apps charts/nebari-apps \
--namespace nebari-apps --create-namespace \
--set clusterDomain=example.ai \
--set keycloak.url=https://keycloak.example.ai/auth
--set keycloak.url=https://keycloak.example.ai/auth \
--set operator.image.tag=latest --set api.image.tag=latest \
--set ui.image.tag=latest --set mcp.image.tag=latest

# The nebari-operator only processes namespaces that opt in:
kubectl label namespace nebari-apps nebari.dev/managed=true
Expand Down
7 changes: 6 additions & 1 deletion mcp/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "nebari-apps-mcp"
version = "0.1.0"
dynamic = ["version"]
description = "MCP server for the Nebari Apps Pack - launch and manage web apps from coding agents"
requires-python = ">=3.12"
dependencies = [
Expand Down Expand Up @@ -28,6 +28,11 @@ dev = [
requires = ["hatchling"]
build-backend = "hatchling.build"

# The version lives in __init__.py (release-please bumps it), so uv.lock
# does not record it and a release needs no relock.
[tool.hatch.version]
path = "src/nebari_apps_mcp/__init__.py"

[tool.hatch.build.targets.wheel]
packages = ["src/nebari_apps_mcp"]

Expand Down
2 changes: 1 addition & 1 deletion mcp/src/nebari_apps_mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""nebari-apps-mcp - agent-facing MCP server for the Nebari Apps Pack."""

__version__ = "0.1.0"
__version__ = "0.1.0" # x-release-please-version
2 changes: 2 additions & 0 deletions mcp/src/nebari_apps_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from starlette.requests import Request
from starlette.responses import JSONResponse, Response

from . import __version__
from .auth import MissingSessionError, TokenVerificationError, auth, get_validator
from .client import ApiClient, ApiError
from .config import settings
Expand All @@ -33,6 +34,7 @@

mcp = FastMCP(
name="nebari-apps",
version=__version__,
instructions=(
"Launch and manage web apps on this Nebari cluster: static sites (HTML/CSS/JS) "
"and Python apps launched by a pixi task (pixi_task; the task must serve on "
Expand Down
1 change: 0 additions & 1 deletion mcp/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"release-type": "simple",
"bootstrap-sha": "45856542a75d2ac69dab2cbe72dc64a6dadb130d",
"bump-minor-pre-major": true,
"include-component-in-tag": true,
"include-v-in-tag": false,
"pull-request-title-pattern": "chore: release ${version}",
"changelog-sections": [
{ "type": "feat", "section": "Features" },
{ "type": "fix", "section": "Bug fixes" },
{ "type": "perf", "section": "Performance" },
{ "type": "revert", "section": "Reverts" },
{ "type": "docs", "section": "Documentation" },
{ "type": "deps", "section": "Dependencies" },
{ "type": "ci", "section": "CI", "hidden": true },
{ "type": "chore", "section": "Miscellaneous", "hidden": true },
{ "type": "test", "section": "Tests", "hidden": true }
],
"packages": {
".": {
"component": "nebari-apps",
"package-name": "nebari-apps",
"extra-files": [
"charts/nebari-apps/Chart.yaml",
"api/src/nebari_apps_api/__init__.py",
"mcp/src/nebari_apps_mcp/__init__.py",
{ "type": "json", "path": "ui/package.json", "jsonpath": "$.version" },
{ "type": "json", "path": "ui/package-lock.json", "jsonpath": "$.version" },
{ "type": "json", "path": "ui/package-lock.json", "jsonpath": "$.packages[''].version" }
]
}
}
}
Loading
Loading