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
260 changes: 221 additions & 39 deletions .github/workflows/docker.yml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ This file is the canonical agent instruction file for the repository.
| **Build, Test & Infra** | [`docs/agents/infra.md`](docs/agents/infra.md) | building/testing, or editing Terraform/Helm/deploy under `platform/`. |
| **Repository context** | [`docs/agents/repository-context.md`](docs/agents/repository-context.md) | starting or resuming a long-running task, tracing an unfamiliar capability, recording durable context, or deciding whether semantic context belongs in CI. |
| **Workflow & Review Gate** | [`docs/agents/workflow.md`](docs/agents/workflow.md) | planning (`plan.md`/`CHANGELOG.md`), creating or updating a PR/stack, writing commit messages, or hitting the review gate. |
| **Release versions & migrations** | [`docs/agents/versioning.md`](docs/agents/versioning.md) | changing any application, shared dependency, Helm chart, database schema, release manifest, or version; every directly or dependency-adapted app and every upgrade path is recorded here. |
| **Release versions & migrations** | [`docs/agents/versioning.md`](docs/agents/versioning.md) | changing any application, shared dependency, Helm chart, database schema, release manifest, or version; every directly changed app and every upgrade path is recorded here. |
| **Language-neutral maintainability** | [`docs/agents/maintainability.md`](docs/agents/maintainability.md) | adding substantial production code in any language, growing an already-large module, or reviewing cohesion and responsibility boundaries. |
| **App-Specific** | [`docs/agents/app-specific.md`](docs/agents/app-specific.md) | working inside a specific `apps/*` or `libs/*` package; per-package map + API-first rule. |
| **Package docs** | [`docs/agents/package-docs.md`](docs/agents/package-docs.md) | writing or editing any package `README.md`, or adding/moving/deleting a package — the README standard, the junior-dev voice, and the "update the README in the same change" rule. |
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified apps/_infra/deploy-k8s/charts/opencrane-artifact-service-0.8.0.tgz
Binary file not shown.
36 changes: 28 additions & 8 deletions apps/_infra/deploy-k8s/platform/database-release-finalization.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Owns application finalization after the database release transition. It restores the exact fenced
# Helm revision when un-fencing, Secret-triggered restarts, rollout waits, certificate readiness, or
# Helm revision when un-fencing, credential-checksum rolls, rollout waits, certificate readiness, or
# verification fail.

capture_fenced_main_release_revision()
Expand Down Expand Up @@ -66,32 +66,52 @@ run_opencrane_finalization_stage()
"$@"
}

restart_database_consumers_for_finalization()
# Digests the published connection Secrets so the consumer roll below can tell whether the
# credentials the running pods loaded are still current. Credential bytes flow straight from
# kubectl into the digest through the pipe; the deploy shell never holds them in a variable
# or argument.
compute_database_connection_checksum()
{
local namespace="$1"
shift
kubectl get secret "$@" -n "$namespace" \
-o jsonpath='{range .items[*]}{.metadata.name}{":"}{.data}{"\n"}{end}' \
| LC_ALL=C sort | sha256sum | cut -d' ' -f1
}

# Stamps the connection-Secret checksum onto each consumer Deployment's pod template. An
# unchanged checksum is a server-side no-op, so pods the preceding helm upgrade just started
# keep running; a changed checksum triggers exactly one rollout. The previous unconditional
# `rollout restart` here forced a second full startup of the heaviest workloads on every
# deploy, even when no credential changed.
roll_database_consumers_for_finalization()
{
local namespace="$1"
local timeout="$2"
local checksum="$3"
local command_status
local deployment
local deployment_resource
shift 2
shift 3
for deployment in "$@"; do
if deployment_resource="$(kubectl get "deployment/$deployment" -n "$namespace" --ignore-not-found -o name)"; then
command_status=0
else
command_status=$?
fi
if (( command_status != 0 )); then
err "Unable to inventory database consumer Deployment '$deployment' before restart."
err "Unable to inventory database consumer Deployment '$deployment' before the credential roll."
return "$command_status"
fi
if [[ -n "$deployment_resource" ]]; then
if kubectl rollout restart "deployment/$deployment" -n "$namespace"; then
if kubectl patch "deployment/$deployment" -n "$namespace" --type merge \
-p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"opencrane.ai/database-connection-checksum\":\"$checksum\"}}}}}"; then
command_status=0
else
command_status=$?
fi
if (( command_status != 0 )); then
err "Unable to restart database consumer Deployment '$deployment'."
err "Unable to stamp the database connection checksum on Deployment '$deployment'."
return "$command_status"
fi
fi
Expand All @@ -103,7 +123,7 @@ restart_database_consumers_for_finalization()
command_status=$?
fi
if (( command_status != 0 )); then
err "Unable to inventory database consumer Deployment '$deployment' after restart."
err "Unable to inventory database consumer Deployment '$deployment' after the credential roll."
return "$command_status"
fi
if [[ -n "$deployment_resource" ]]; then
Expand All @@ -113,7 +133,7 @@ restart_database_consumers_for_finalization()
command_status=$?
fi
if (( command_status != 0 )); then
err "Database consumer Deployment '$deployment' did not complete its restart."
err "Database consumer Deployment '$deployment' did not complete its credential roll."
return "$command_status"
fi
fi
Expand Down
22 changes: 19 additions & 3 deletions apps/_infra/deploy-k8s/platform/k8s-deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -978,11 +978,15 @@ helm_args+=(
# --reuse-values → inherit last release verbatim (do NOT refresh chart defaults)
# --reset-values → intentionally DROP prior overrides (start from chart defaults + this run)
# A fresh install has nothing to reuse, so none of these apply.
RELEASE_PREEXISTED=0
if helm status "$RELEASE" -n "$NAMESPACE" >/dev/null 2>&1; then
RELEASE_PREEXISTED=1
fi
if [[ -n "$REUSE_VALUES" ]]; then
helm_args+=(--reuse-values)
elif [[ -n "$RESET_VALUES" ]]; then
helm_args+=(--reset-values)
elif helm status "$RELEASE" -n "$NAMESPACE" >/dev/null 2>&1; then
elif [[ "$RELEASE_PREEXISTED" == "1" ]]; then
log "Existing release '$RELEASE' — using --reset-then-reuse-values so prior overrides are not silently dropped (pass --reset-values to start from chart defaults instead)."
helm_args+=(--reset-then-reuse-values)
fi
Expand All @@ -1001,8 +1005,20 @@ fi
append_authoritative_qualified_release_image_helm_args
append_authoritative_cognee_image_helm_args
run_opencrane_finalization_stage helm "${helm_args[@]}" || exit $?
run_opencrane_finalization_stage restart_database_consumers_for_finalization "$NAMESPACE" "$TIMEOUT" \
"${RELEASE}-opencrane-server" "${RELEASE}-litellm" "${RELEASE}-mcp-gateway" || exit $?
# The database consumers load their connection Secrets at startup, and Helm does not roll pods
# when only a Secret published outside the chart changed. Stamping the Secret checksum onto the
# pod templates rolls the consumers exactly when the credentials changed, instead of restarting
# pods the upgrade above just started. A fresh install skips the roll entirely: its pods were
# born after this run published the Secrets, so there is nothing to propagate, and stamping
# mid-first-rollout forced a second boot of the heaviest workloads.
if [[ "$RELEASE_PREEXISTED" == "1" ]]; then
DATABASE_CONNECTION_CHECKSUM="$(compute_database_connection_checksum "$NAMESPACE" \
"$POSTGRES_APP_SECRET" "$OBOT_POSTGRES_APP_SECRET" "$LITELLM_POSTGRES_APP_SECRET" \
"$POSTGRES_ADMIN_APP_SECRET")" || exit $?
run_opencrane_finalization_stage roll_database_consumers_for_finalization "$NAMESPACE" "$TIMEOUT" \
"$DATABASE_CONNECTION_CHECKSUM" \
"${RELEASE}-opencrane-server" "${RELEASE}-litellm" "${RELEASE}-mcp-gateway" || exit $?
fi

# 4. Wait for the core workloads. The database schema was created by CNPG initdb or converged by
# the bounded deployment-owned migration Job; application startup never mutates it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,13 @@ export SUCCESS_CALLS
deployment_name="${2#deployment/}"
printf 'inventory %s\n' "$deployment_name" >>"$SUCCESS_CALLS"
printf 'deployment.apps/%s\n' "$deployment_name"
elif [[ "$1 $2" == "rollout restart" ]]; then
deployment_name="${3#deployment/}"
printf 'restart %s\n' "$deployment_name" >>"$SUCCESS_CALLS"
elif [[ "$1 $2" == "patch deployment/"* ]]; then
deployment_name="${2#deployment/}"
if [[ "$*" != *'opencrane.ai/database-connection-checksum'* || "$*" != *'checksum-value'* ]]; then
printf 'unexpected finalization patch payload: %s\n' "$*" >&2
return 1
fi
printf 'patch %s\n' "$deployment_name" >>"$SUCCESS_CALLS"
elif [[ "$1 $2" == "rollout status" ]]; then
deployment_name="${3#deployment/}"
printf 'rollout %s\n' "$deployment_name" >>"$SUCCESS_CALLS"
Expand All @@ -342,8 +346,8 @@ export SUCCESS_CALLS
BOUNDARY_PHASE=finalization
run_opencrane_finalization_stage helm upgrade opencrane /chart \
--set clustertenantManager.replicas=2 --set migrationFence.active=false
run_opencrane_finalization_stage restart_database_consumers_for_finalization opencrane 37 \
opencrane-opencrane-server opencrane-litellm
run_opencrane_finalization_stage roll_database_consumers_for_finalization opencrane 37 \
checksum-value opencrane-opencrane-server opencrane-litellm
run_opencrane_finalization_stage wait_for_final_deployment_if_present opencrane-clustertenant-manager
run_opencrane_finalization_stage _wait_for_release_certificate
run_opencrane_finalization_stage _post_deploy_verify
Expand All @@ -362,9 +366,9 @@ printf '%s\n' \
helm-capture-fenced \
helm-unfence \
'inventory opencrane-opencrane-server' \
'restart opencrane-opencrane-server' \
'patch opencrane-opencrane-server' \
'inventory opencrane-litellm' \
'restart opencrane-litellm' \
'patch opencrane-litellm' \
'inventory opencrane-opencrane-server' \
'rollout opencrane-opencrane-server' \
'inventory opencrane-litellm' \
Expand Down Expand Up @@ -844,7 +848,7 @@ set +e
kubectl() { return 29; }
helm() { printf 'helm %s\n' "$*" >>"$FINAL_CALLS"; }
err() { :; }
run_opencrane_finalization_stage restart_database_consumers_for_finalization opencrane 37 opencrane-server
run_opencrane_finalization_stage roll_database_consumers_for_finalization opencrane 37 checksum-value opencrane-server
)
final_inventory_status=$?
set -e
Expand Down
37 changes: 24 additions & 13 deletions apps/_infra/deploy-k8s/platform/tests/develop-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,19 @@ _build_image()
local image="$2"
local dockerfile="$3"
local cache_arguments=()
if [[ -n "${ACTIONS_RUNTIME_TOKEN:-}" \
&& ( -n "${ACTIONS_RESULTS_URL:-}" || -n "${ACTIONS_CACHE_URL:-}" ) ]]; then
cache_arguments=(
--cache-from "type=gha,scope=${project},timeout=2m"
)
# CI shares registry layer caches per deployable with the publish jobs (see BUILD_CACHE_IMAGE
# in docker.yml). SMOKE_BUILD_CACHE is the trusted cache that integration pushes maintain;
# SMOKE_BUILD_CACHE_UNTRUSTED adds the pull-request cache as a second read source; and
# SMOKE_BUILD_CACHE_EXPORT names where this run may write its layers, so the next push builds
# warm. Local runs leave all three unset and build without a remote cache.
if [[ -n "${SMOKE_BUILD_CACHE:-}" ]]; then
cache_arguments+=(--cache-from "type=registry,ref=${SMOKE_BUILD_CACHE}:${project}")
fi
if [[ -n "${SMOKE_BUILD_CACHE_UNTRUSTED:-}" ]]; then
cache_arguments+=(--cache-from "type=registry,ref=${SMOKE_BUILD_CACHE_UNTRUSTED}:${project}")
fi
if [[ -n "${SMOKE_BUILD_CACHE_EXPORT:-}" ]]; then
cache_arguments+=(--cache-to "type=registry,ref=${SMOKE_BUILD_CACHE_EXPORT}:${project},mode=max")
fi
echo "[develop-smoke] Building $image"
_retry 3 docker buildx build --load --file "$ROOT_DIR/$dockerfile" --tag "$image" \
Expand Down Expand Up @@ -377,6 +385,12 @@ EOF
_wait_for_job "$job_name"
}

# Proves the public health report is complete and every service the smoke can provision is
# healthy. Model routing is the one exception: CI holds no provider credentials, so LiteLLM
# serves an empty estate and the models probe reports unavailable. Seeding a placeholder key
# instead made the server fetch a BYOK Secret through the API server and exit fatally when that
# call failed, so the report is asserted as-is and models is allowed to be unavailable. Reporting
# an unconfigured estate as disabled rather than unavailable is tracked separately.
_assert_ingress_health()
{
local health_url="https://${CONTROL_PLANE_HOST}:8443/healthz"
Expand All @@ -385,10 +399,12 @@ _assert_ingress_health()
until response="$(curl --connect-timeout 2 --max-time 5 --fail --silent --show-error --insecure \
--resolve "${CONTROL_PLANE_HOST}:8443:127.0.0.1" "$health_url" 2>/dev/null)" \
&& jq -e '
.status == "ok"
and .ready == true
.ready == true
and (.services | keys == ["api", "channels", "database", "files", "integrations", "memory", "models"])
and ([.services[]] | all(. == "available" or . == "disabled"))
and ([.services | to_entries[] | select(.key != "models") | .value]
| all(. == "available" or . == "disabled"))
and (.services.models == "available" or .services.models == "unavailable")
and (.status == "ok" or (.status == "degraded" and .services.models != "available"))
' >/dev/null <<<"$response"; do
if [[ $(date +%s) -ge "$deadline" ]]; then
echo "[develop-smoke] Timed out waiting for the complete public health report at $health_url; last response: $response" >&2
Expand Down Expand Up @@ -481,10 +497,6 @@ export OPENCRANE_OIDC_SESSION_SECRET="$(_random_secret)"
# production deploy path still requires a UI digest; this explicit escape keeps the smoke honest.
export OPENCRANE_ALLOW_TAG_FLOAT=1
export TIMEOUT_SECONDS
# The public health report only turns "models" available once LiteLLM lists a routable model.
# Seeding the initial provider with a placeholder key is safe here: registration writes the
# LiteLLM model row without calling the provider, and the health probe lists the estate.
export OPENCRANE_INITIAL_MODEL_API_KEY="sk-develop-smoke-placeholder"
# Exercise the production wrapper's required contact and first-owner inputs. The disposable `.test`
# host cannot complete public ACME, so the final --set flags deliberately restore its local issuer.
"$ROOT_DIR/apps/_infra/deploy-k8s/deploy.sh" \
Expand All @@ -496,7 +508,6 @@ export OPENCRANE_INITIAL_MODEL_API_KEY="sk-develop-smoke-placeholder"
--release "$RELEASE_NAME" \
--release-version "$(jq -r '.version' "$ROOT_DIR/package.json")" \
--from-release-version fresh \
--initial-model-provider openai \
--image-tag develop-smoke \
--cognee-tag develop-smoke \
--storage-class "$SMOKE_STORAGE_CLASS" \
Expand Down
2 changes: 1 addition & 1 deletion apps/agent-controller/helm/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ name: opencrane-agent-controller
description: App-owned named-template library for the personal-runtime workload controller.
type: library
version: 0.9.2
appVersion: "0.9.2"
appVersion: "0.9.1"
2 changes: 1 addition & 1 deletion apps/agent-controller/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@opencrane/agent-controller",
"version": "0.9.2",
"version": "0.9.1",
"private": true,
"license": "AGPL-3.0-or-later",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion apps/agent-controller/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "agent-controller",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"metadata": { "release": { "adaptedVersion": "0.9.2" } },
"metadata": { "release": { "adaptedVersion": "0.9.1" } },
"sourceRoot": "apps/agent-controller/src",
"tags": ["type:app", "layer:entrypoint", "scope:agent-controller"],
"targets": {
Expand Down
2 changes: 1 addition & 1 deletion apps/artifact-preprocessor/helm/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ name: opencrane-artifact-preprocessor
description: App-owned named-template library for the isolated PDF preprocessing worker.
type: library
version: 0.9.2
appVersion: "0.9.2"
appVersion: "0.9.1"
2 changes: 1 addition & 1 deletion apps/artifact-preprocessor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@opencrane/artifact-preprocessor",
"version": "0.9.2",
"version": "0.9.1",
"private": true,
"license": "AGPL-3.0-or-later",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion apps/artifact-preprocessor/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "artifact-preprocessor",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"metadata": { "release": { "adaptedVersion": "0.9.2" } },
"metadata": { "release": { "adaptedVersion": "0.9.1" } },
"sourceRoot": "apps/artifact-preprocessor/src",
"tags": ["type:app", "layer:entrypoint", "scope:artifacts"],
"targets": {
Expand Down
2 changes: 1 addition & 1 deletion apps/artifact-scanner/helm/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ name: opencrane-artifact-scanner
description: App-owned named-template library for the isolated malware-scanning worker.
type: library
version: 0.9.2
appVersion: "0.9.2"
appVersion: "0.9.1"
2 changes: 1 addition & 1 deletion apps/artifact-scanner/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@opencrane/artifact-scanner",
"version": "0.9.2",
"version": "0.9.1",
"private": true,
"license": "AGPL-3.0-or-later",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion apps/artifact-scanner/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "artifact-scanner",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"metadata": { "release": { "adaptedVersion": "0.9.2" } },
"metadata": { "release": { "adaptedVersion": "0.9.1" } },
"sourceRoot": "apps/artifact-scanner/src",
"tags": ["type:app", "layer:entrypoint", "scope:artifacts"],
"targets": {
Expand Down
6 changes: 4 additions & 2 deletions apps/artifact-service/deploy/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ WORKDIR /app
COPY package.json package-lock.json tsconfig.json nx.json ./
COPY libs libs
COPY apps/artifact-service/package.json apps/artifact-service/project.json apps/artifact-service/tsconfig.json apps/artifact-service/
RUN npm ci
# The cache mount keeps npm's download cache out of the image layers and shares it
# between stages, so the runtime-stage install reuses the packages this stage fetched.
RUN --mount=type=cache,target=/root/.npm npm ci

COPY apps/artifact-service/src apps/artifact-service/src
RUN npx nx run artifact-service:build
Expand All @@ -16,7 +18,7 @@ WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
COPY apps/artifact-service/package.json apps/artifact-service/
RUN npm ci --omit=dev --workspace=apps/artifact-service
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev --workspace=apps/artifact-service

COPY --from=build /app/dist/apps/artifact-service dist/apps/artifact-service

Expand Down
2 changes: 1 addition & 1 deletion apps/artifact-service/helm/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ name: opencrane-artifact-service
description: App-owned named-template library for canonical artifact bytes.
type: library
version: 0.8.0
appVersion: "0.8.0"
appVersion: "0.9.2"
2 changes: 1 addition & 1 deletion apps/artifact-service/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@opencrane/artifact-service",
"version": "0.8.0",
"version": "0.9.2",
"private": true,
"license": "AGPL-3.0-or-later",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion apps/artifact-service/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "artifact-service",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"metadata": { "release": { "adaptedVersion": "0.8.0" } },
"metadata": { "release": { "adaptedVersion": "0.9.2" } },
"sourceRoot": "apps/artifact-service/src",
"tags": ["type:app", "layer:entrypoint", "scope:artifacts"],
"targets": {
Expand Down
6 changes: 4 additions & 2 deletions apps/channel-proxy/deploy/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ WORKDIR /app
COPY package.json package-lock.json tsconfig.json nx.json ./
COPY libs libs
COPY apps/channel-proxy/package.json apps/channel-proxy/project.json apps/channel-proxy/tsconfig.json apps/channel-proxy/
RUN npm ci
# The cache mount keeps npm's download cache out of the image layers and shares it
# between stages, so the runtime-stage install reuses the packages this stage fetched.
RUN --mount=type=cache,target=/root/.npm npm ci

COPY apps/channel-proxy/src apps/channel-proxy/src
RUN npx nx run channel-proxy:build
Expand All @@ -16,7 +18,7 @@ WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
COPY apps/channel-proxy/package.json apps/channel-proxy/
RUN npm ci --omit=dev --workspace=apps/channel-proxy
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev --workspace=apps/channel-proxy

COPY --from=build /app/dist/apps/channel-proxy dist/apps/channel-proxy

Expand Down
Loading
Loading