From c77edf0fa451569c06f1096c5a723581dc36aabb Mon Sep 17 00:00:00 2001 From: Jente Rosseel Date: Wed, 19 Aug 2026 14:44:30 +0300 Subject: [PATCH 1/9] =?UTF-8?q?=E2=9A=A1=20cache=20image=20layers=20in=20t?= =?UTF-8?q?he=20registry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 10GB Actions cache quota evicted buildx layer blobs on almost every run, so the k3d smoke and the publish jobs rebuilt every image cold. The layer cache now lives in one ghcr repository (opencrane-buildcache) with a tag per deployable, the smoke exports its layers for the next run, and the npm download cache is shared between Dockerfile stages. --- .github/workflows/docker.yml | 29 ++++++++++++------- .../platform/tests/develop-smoke.sh | 14 +++++---- apps/artifact-service/deploy/Dockerfile | 6 ++-- apps/channel-proxy/deploy/Dockerfile | 6 ++-- apps/memory-gateway/deploy/Dockerfile | 6 ++-- apps/opencrane-ui/deploy/Dockerfile | 4 ++- apps/opencrane/deploy/Dockerfile | 6 ++-- 7 files changed, 46 insertions(+), 25 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 1b8094ffb..2bfe7d380 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -45,6 +45,10 @@ permissions: env: REGISTRY: ghcr.io + # Image layer cache lives in one registry repository with one tag per deployable. The registry + # backend replaced the GitHub Actions cache backend because the 10GB Actions cache quota evicted + # the large layer blobs on almost every run, so every build started cold. + BUILD_CACHE_IMAGE: opencrane-buildcache jobs: prepare: @@ -325,7 +329,10 @@ jobs: needs: prepare permissions: contents: read - packages: read + # Write lets the smoke publish its layer cache so the next run builds warm. GitHub + # downgrades the token to read-only for pull requests from forks, and the smoke then + # skips the cache export (see SMOKE_BUILD_CACHE_PUSH below). + packages: write # Every accepted develop commit proves the current deployment path. A pull request skips the # live cluster only when its exact base SHA already completed the same k3d job successfully. # Missing or malformed proof output deliberately compares unequal to true and runs k3d. @@ -384,11 +391,6 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - # Docker's GHA cache backend requires these runtime variables for an inline buildx command. - # Pin the helper by commit so the remote action cannot move underneath qualification. - - name: Expose the GitHub Actions cache runtime - uses: crazy-max/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487 - - name: Run the current-silo smoke run: ./apps/_infra/deploy-k8s/platform/tests/develop-smoke.sh env: @@ -398,6 +400,9 @@ jobs: SMOKE_BASE_SHA: ${{ needs.prepare.outputs.nx_base }} SMOKE_REGISTRY: ${{ env.REGISTRY }}/${{ github.repository_owner }} SMOKE_STORAGE_MODE: ${{ needs.prepare.outputs.develop_smoke_storage_mode }} + SMOKE_BUILD_CACHE: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.BUILD_CACHE_IMAGE }} + # Fork pull requests get a read-only token, so they consume the cache without exporting. + SMOKE_BUILD_CACHE_PUSH: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && '1' || '0' }} image_smoke: name: Image smoke (${{ matrix.project }}) @@ -453,8 +458,9 @@ jobs: steps: - uses: actions/checkout@v6 + # Validation-only runs also log in: pulling the registry layer cache needs an + # authenticated token even when nothing is pushed. - name: Log in to GitHub Container Registry - if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_deployables != 'none') uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} @@ -481,8 +487,9 @@ jobs: push: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_deployables != 'none') }} tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} - cache-from: type=gha,scope=${{ matrix.project }} - cache-to: type=gha,mode=max,scope=${{ matrix.project }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.BUILD_CACHE_IMAGE }}:${{ matrix.project }} + # Fork pull requests get a read-only token, so they consume the cache without exporting. + cache-to: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && format('type=registry,ref={0}/{1}/{2}:{3},mode=max', env.REGISTRY, github.repository_owner, env.BUILD_CACHE_IMAGE, matrix.project) || '' }} publish-develop-smoke-images: name: Publish develop smoke image (${{ matrix.project }}) @@ -551,5 +558,5 @@ jobs: file: ${{ matrix.dockerfile }} push: true tags: ${{ steps.image-set.outputs.current_ref }} - cache-from: type=gha,scope=${{ matrix.project }} - cache-to: type=gha,mode=max,scope=${{ matrix.project }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.BUILD_CACHE_IMAGE }}:${{ matrix.project }} + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.BUILD_CACHE_IMAGE }}:${{ matrix.project }},mode=max diff --git a/apps/_infra/deploy-k8s/platform/tests/develop-smoke.sh b/apps/_infra/deploy-k8s/platform/tests/develop-smoke.sh index 09997a584..30e50c385 100755 --- a/apps/_infra/deploy-k8s/platform/tests/develop-smoke.sh +++ b/apps/_infra/deploy-k8s/platform/tests/develop-smoke.sh @@ -153,11 +153,15 @@ _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 one registry layer cache per deployable with the publish jobs (see + # BUILD_CACHE_IMAGE in docker.yml). The smoke also exports its layers when the token can + # write, so the next pull-request push builds warm instead of cold. Local runs leave + # SMOKE_BUILD_CACHE unset and build without a remote cache. + if [[ -n "${SMOKE_BUILD_CACHE:-}" ]]; then + cache_arguments+=(--cache-from "type=registry,ref=${SMOKE_BUILD_CACHE}:${project}") + if [[ "${SMOKE_BUILD_CACHE_PUSH:-0}" == "1" ]]; then + cache_arguments+=(--cache-to "type=registry,ref=${SMOKE_BUILD_CACHE}:${project},mode=max") + fi fi echo "[develop-smoke] Building $image" _retry 3 docker buildx build --load --file "$ROOT_DIR/$dockerfile" --tag "$image" \ diff --git a/apps/artifact-service/deploy/Dockerfile b/apps/artifact-service/deploy/Dockerfile index 3ee11e0a3..2db57730f 100644 --- a/apps/artifact-service/deploy/Dockerfile +++ b/apps/artifact-service/deploy/Dockerfile @@ -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 @@ -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 diff --git a/apps/channel-proxy/deploy/Dockerfile b/apps/channel-proxy/deploy/Dockerfile index 9d3cdef70..acb50b84f 100644 --- a/apps/channel-proxy/deploy/Dockerfile +++ b/apps/channel-proxy/deploy/Dockerfile @@ -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 @@ -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 diff --git a/apps/memory-gateway/deploy/Dockerfile b/apps/memory-gateway/deploy/Dockerfile index a5b47423c..4be4824d5 100644 --- a/apps/memory-gateway/deploy/Dockerfile +++ b/apps/memory-gateway/deploy/Dockerfile @@ -5,7 +5,9 @@ WORKDIR /app COPY package.json package-lock.json tsconfig.json nx.json ./ COPY libs libs COPY apps/memory-gateway/package.json apps/memory-gateway/project.json apps/memory-gateway/tsconfig.json apps/memory-gateway/ -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/memory-gateway/src apps/memory-gateway/src RUN npx nx run memory-gateway:build @@ -16,7 +18,7 @@ WORKDIR /app ENV NODE_ENV=production COPY package.json package-lock.json ./ COPY apps/memory-gateway/package.json apps/memory-gateway/ -RUN npm ci --omit=dev --workspace=apps/memory-gateway +RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev --workspace=apps/memory-gateway # Shared conversation contracts keep these packages external in the compiled gateway bundle. # Resolve their public runtime entrypoints now so a production image cannot build without them. RUN node --input-type=module --eval \ diff --git a/apps/opencrane-ui/deploy/Dockerfile b/apps/opencrane-ui/deploy/Dockerfile index 7c327a4af..0446f1b9c 100644 --- a/apps/opencrane-ui/deploy/Dockerfile +++ b/apps/opencrane-ui/deploy/Dockerfile @@ -19,7 +19,9 @@ COPY package.json package-lock.json tsconfig.json tsconfig.frontend.json nx.json COPY libs libs COPY apps/opencrane-ui apps/opencrane-ui # npm workspaces install: root package.json's "workspaces" field includes apps/* and website -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 RUN npx nx build opencrane-ui diff --git a/apps/opencrane/deploy/Dockerfile b/apps/opencrane/deploy/Dockerfile index 4884bd25d..a3d650947 100644 --- a/apps/opencrane/deploy/Dockerfile +++ b/apps/opencrane/deploy/Dockerfile @@ -17,7 +17,9 @@ COPY apps/opencrane/scripts apps/opencrane/scripts COPY apps/opencrane/package.json apps/opencrane/tsconfig.json apps/opencrane/prisma.config.ts apps/opencrane/ COPY apps/opencrane/prisma apps/opencrane/prisma # npm workspaces install: root package.json's "workspaces" field includes apps/* and website -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/opencrane/src apps/opencrane/src # Generate Prisma client, build dependencies first, then the app @@ -39,7 +41,7 @@ COPY package.json package-lock.json ./ # database baseline are build/provisioning inputs; server startup never mutates schema. COPY apps/opencrane/package.json apps/opencrane/ # Install only the app workspace's runtime dependencies -RUN npm ci --omit=dev --workspace=apps/opencrane +RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev --workspace=apps/opencrane # Shared conversation contracts keep these packages external in the compiled server bundle. # Resolve their public runtime entrypoints now so a production image cannot build without them. RUN node --input-type=module --eval \ From d063570de929f25f5d19013035a2d29e8cc23e35 Mon Sep 17 00:00:00 2001 From: Jente Rosseel Date: Wed, 19 Aug 2026 14:44:30 +0300 Subject: [PATCH 2/9] =?UTF-8?q?=E2=9A=A1=20roll=20database=20consumers=20o?= =?UTF-8?q?nly=20on=20credential=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every deploy restarted opencrane-server, litellm, and mcp-gateway right after the helm upgrade that had just rolled them, forcing a second full startup of the heaviest workloads. The finalization now stamps a checksum of the published connection Secrets onto the pod templates: an unchanged checksum is a server-side no-op, a changed one triggers exactly one rollout, and a run that failed between publish and roll self-heals on the next deploy. --- .../platform/database-release-finalization.sh | 36 ++++++++++++++----- apps/_infra/deploy-k8s/platform/k8s-deploy.sh | 10 +++++- .../database-migration-deploy-contract.sh | 20 ++++++----- 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/apps/_infra/deploy-k8s/platform/database-release-finalization.sh b/apps/_infra/deploy-k8s/platform/database-release-finalization.sh index f4b465d7d..ee0351579 100755 --- a/apps/_infra/deploy-k8s/platform/database-release-finalization.sh +++ b/apps/_infra/deploy-k8s/platform/database-release-finalization.sh @@ -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() @@ -66,14 +66,33 @@ 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}' \ + | 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 @@ -81,17 +100,18 @@ restart_database_consumers_for_finalization() 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 @@ -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 @@ -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 diff --git a/apps/_infra/deploy-k8s/platform/k8s-deploy.sh b/apps/_infra/deploy-k8s/platform/k8s-deploy.sh index 93828da79..9da2a6c4e 100755 --- a/apps/_infra/deploy-k8s/platform/k8s-deploy.sh +++ b/apps/_infra/deploy-k8s/platform/k8s-deploy.sh @@ -1001,7 +1001,15 @@ 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" \ +# 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. +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 $? # 4. Wait for the core workloads. The database schema was created by CNPG initdb or converged by diff --git a/apps/_infra/deploy-k8s/platform/tests/database-migration-deploy-contract.sh b/apps/_infra/deploy-k8s/platform/tests/database-migration-deploy-contract.sh index 1313aab28..e0ffd8876 100755 --- a/apps/_infra/deploy-k8s/platform/tests/database-migration-deploy-contract.sh +++ b/apps/_infra/deploy-k8s/platform/tests/database-migration-deploy-contract.sh @@ -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" @@ -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 @@ -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' \ @@ -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 From ab98437f84b2dfb74634e8bc44d067def3097824 Mon Sep 17 00:00:00 2001 From: Jente Rosseel Date: Wed, 19 Aug 2026 14:44:30 +0300 Subject: [PATCH 3/9] =?UTF-8?q?=E2=9A=A1=20stamp=20only=20directly=20chang?= =?UTF-8?q?ed=20applications?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A shared library, root dependency, or lockfile change forced every application to restate the root version, so most CI failures were manifest bookkeeping instead of real defects. The release gate now requires the root-version stamp only from applications whose own files changed; untouched applications keep their latest released version, which images pinned by commit SHA already made safe. The 0.9.2 manifest moves with the policy: artifact-service and channel-proxy stamp to 0.9.2 for their direct changes since 0.9.1, while agent-controller, artifact-preprocessor, and artifact-scanner return to 0.9.1 because only propagation had bumped them. --- AGENTS.md | 2 +- .../opencrane-agent-controller-0.9.2.tgz | Bin 8652 -> 8644 bytes .../opencrane-artifact-preprocessor-0.9.2.tgz | Bin 2183 -> 2178 bytes .../opencrane-artifact-scanner-0.9.2.tgz | Bin 2296 -> 2292 bytes .../opencrane-artifact-service-0.8.0.tgz | Bin 1786 -> 1796 bytes .../charts/opencrane-channel-proxy-0.8.0.tgz | Bin 2032 -> 2043 bytes apps/agent-controller/helm/Chart.yaml | 2 +- apps/agent-controller/package.json | 2 +- apps/agent-controller/project.json | 2 +- apps/artifact-preprocessor/helm/Chart.yaml | 2 +- apps/artifact-preprocessor/package.json | 2 +- apps/artifact-preprocessor/project.json | 2 +- apps/artifact-scanner/helm/Chart.yaml | 2 +- apps/artifact-scanner/package.json | 2 +- apps/artifact-scanner/project.json | 2 +- apps/artifact-service/helm/Chart.yaml | 2 +- apps/artifact-service/package.json | 2 +- apps/artifact-service/project.json | 2 +- apps/channel-proxy/helm/Chart.yaml | 2 +- apps/channel-proxy/package.json | 2 +- apps/channel-proxy/project.json | 2 +- docs/agents/versioning.md | 31 ++++++------ releases/0.9.2.json | 30 ++++++------ .../release-versioning-check.test.mjs | 16 +++---- scripts/release-versioning/core.mjs | 45 +++++------------- 25 files changed, 67 insertions(+), 87 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 17e6efaeb..7b2ba2fd1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. | diff --git a/apps/_infra/deploy-k8s/charts/opencrane-agent-controller-0.9.2.tgz b/apps/_infra/deploy-k8s/charts/opencrane-agent-controller-0.9.2.tgz index acc3826a0496890fc54a89e78961fff2a2f5d27e..be26a8acc0c80375fd5c7583c8f1943a7bcf5eea 100644 GIT binary patch delta 8642 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 diff --git a/apps/_infra/deploy-k8s/charts/opencrane-artifact-preprocessor-0.9.2.tgz b/apps/_infra/deploy-k8s/charts/opencrane-artifact-preprocessor-0.9.2.tgz index 0bf4be1e616e4d10b839e09c1749ead9429bc689..b714996bbe9b01a7f690e22dfe97d8a88ff5743d 100644 GIT binary patch delta 2141 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 Y*~(V7@+X%61^@v6{{>F#9{@H0089E$kpKVy diff --git a/apps/_infra/deploy-k8s/charts/opencrane-artifact-scanner-0.9.2.tgz b/apps/_infra/deploy-k8s/charts/opencrane-artifact-scanner-0.9.2.tgz index d357a5d1f19d0444a60038e4c4240f496213600c..8e7f3394c30edac4448b9d8208b03fde54e14964 100644 GIT binary patch delta 2256 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 iq?G^lGX6Jq{NKq=cCwSdvHULp0RR60Vx11F diff --git a/apps/_infra/deploy-k8s/charts/opencrane-artifact-service-0.8.0.tgz b/apps/_infra/deploy-k8s/charts/opencrane-artifact-service-0.8.0.tgz index 55103f9a1ef8d87abff4124a4817931b2169a22c..786256d1b104405399370d815cf35324928a6073 100644 GIT binary patch delta 1722 zcmV;r21WV$4TKJmJb&G9>$Vlov;T?%p#xf=l4a*(nSnicj@ub?62WoqRt$qdOUDiu zieyR3_qwV7_kofnTe2U|a^+7NZ( zzgbt+x!(y9Nk5^|RPYWg+6^LQ@zeMIQGT>(xI-DuK3I`0#0I#T!XP>#2_#_ zRYWaeCKQB(3r<7AATKGHE)8n0VVOE`xa1mUDY26aYb-XFTzY=vw+_|2q?-MoCs#Ko z3bja7h}tvq<$q{UzyDij=k0dB|NU0`{KRj!PEXH2_W#$QrzaO8oKcS86t&>xwQ!e} zf>-3<0e7`(SSC6H?-OAO>Ydfd$&_IP>y_Wk62@*wjL`WrP#&@*Dk-zm78xAy3hCeE zGA*B$ButqjoRNeX_*N>)%?zAB=%Q@31KtQ35{+KL^ncw!8+jqusw|;ktMN#A)PeQV zZWxM$n?{U=M8uFz10Vykr>8C7R;yh&%NC|;vemm@tyT>HMyANL6%3HZb~i}b5*wml zfm0qKH*l750}v^_$7G6%qs3V&nxTlL;K)q}cB~?a5wUft?BG~DBJaSAFpUk@b@vA) zup`i zAW!#cHQz)^-7J!Rg_4P7jPGd|#atH zp%E&Zj)@5$hFh*3bS~!`?s4)Wck-of0_4Z|+MKQYSz;y2US-?m#+~60p@m(dnt8WA!31K>5RD$KHdyT3CE#LRc<3B2q$jZcQ z?|-Pq(1s&WVn!KmG#6Era6@Cf%Fem?-Jv#)BWaz42spdwW1r zb3s&-(P^`Qi)PW3)yur@{$p|<3~#$vldJntcRaYgnOqNs!@*s@cYAYn_hQdt@_*%C zGC`uNM5VQHO&OyaL%}2cY*&Zv@9rZmbu(>9 zU71W74LynRks$Yb{XsCE1f#*H?zsO}v=ew<=p?}pb2YZqBjMo;J4{V@9T>8qO;&+Pg0WlA+D7sQ) zj>xTvX@xUKj2(=y6O^OYff7?J`)WbrM>JJjl9>UPbNNekF2#-=bfKIYO4t=Lvb@Wx zyaVmh5{Zh6sF*z8nq+i=uQJgxzQw}xItyEVDU78yJ11MXM*Ku#R@1VrIg=;_CV$Ot z%%fms<#^x!LbaiMewQ)>)din(Fp4$&2henVN4Hm}4)|xjUoHoO@y>zl!Xqv_Pz#k% zRNs+hYhlNrZzFHt%7fS6bzI)xH7M&pdo0lg)gaip#=PQQ@JfqH{nehnN-AMQC|C!^ zy&xZZU2A2CnxhFe$W_fdNAp>zM}Nxa-s5Fzc~J>B*;LlWzQ1~q7Los_?y)kB^mr+= zS26=WSE#k~JvjaB$@cOz8_kPnrbJvt_%yU--*VdHV@_pD^K^Sfu6N%(mVt36p3l(v ziukt9s(5@P(3|)FHqxlJHdN_s+_&pqx%04;A|t*+g6yQ5-R9nylgCT#5`QVzA|tMk z2kic8H(=+d&yMYSY2}@JsYmyOLAL$c{VRs{zf}A6MQf0`pn}Plqnhu3F`Z|ZXx;R^mgl=hxapTz@84SR7w9113k9FhYbT7b3Q|^3kr;XU1yv!$UH)Myn5YlP<-pc=jKInr!=>L}f Q6954J|GZAdSpX;i0L`Rz2><{9 delta 1722 zcmV;r21WUV4*Cs{Jbzzr>NpnfGoRupx+~2{lMpDaDx`g2DKm?fwh^UwceGj|lN_kG zj_t7>?qxcB_mvZpk^rH9w0Cy8JueNm&;R3}kCO_CJXD0EOO&B=5*k;dTG9}k{d=N} zw<0k+c^mn@@1J)%>0jUXtAG9W+388^tleq(osNI%pZKlw(|_}`6Y$@t_gR!^L)3}? zW?fb1{vbpo{ftUe!Mm_*H;9zQPv5)n{6>U2R8*RDs3)Z>zHp3ylNck{U@RFi2#n4Y zQ7f1W1tH;r(~vO8OA2NygW79YrY;;VxrSLu?B>E6%d#cEcc|Va)$IQ~xw<)3s70be z)Si(qM}zwP-+$_ywcF+W-)^0rc0Tt1x1h&I7b2Wfj^Grv;N`V&o0WoB3`0 zItTAFVF~J;)yT<=VFa6%pXLc;*Ca;h{uw9_SrV0$*=dUm4tRz1Pq|FXrzHtf<_PB` zVFtdFigGgt=TEvQTkU{1MutSAS1^5l&_-U!wJJ*}*nes~P#$$*v$Pk6BH^YHqahJ7 zq}u?B5dxBrzhkE|ndehzI0dm=mV4;kxerpagbA8|3D*U`dR9 zMrizgVt=D0hE^8|L)7|8M5w#)PiKrI`n#eAZ+M8#zZw9=j>s%i_)CJ?Y<>Y~BNSO= zo*gPdnTIk&Pt#u!euG~UVK#nSU%yau|2+0gU7r!HGxqI~>pUagt_tMoKCR}PNU57e z(l1dmv5N6M?Seeh3MHc<(Y2|{7v-^4#Nc9;nSYw~EMp865@Ws9$S^cQWz#V+;r(dK zwS&&(e4{;1p65=!)J=f=7~hz)l|M_YWZA21yLhtg<<C^4rD zpMNwLRg`c;W4z4Hxy^JaxFM9I+DenV*?b3rVTr~m068^foi4G(Ei^SktzfN}>FT6q4wMvz0A?AAc!Bz0wBbN+|$Hp-e?;o{xKDWJO((zCb8b z8ylS?yHKVa7v#(R`huJBq@r(Wg+E_U=|tF7&>K&NKlb{Q>Gs3xPcW&sz? zqA9DFdDZ*J^e!0P^e(5DcjMk4`ttV0p2g(ron(SUmx)Si<9~`W zMm2_lNBY^Wjt0Hk!8Ex0%V^l2{%!E{H0VwKTI0J)_#WTg2VCi9+K{?3nK2rA65}I5 z9`pypU@{HH!_U3R;H}CPhDfDxrJ_q>a7X_Euop91A=&;}4fxW5?;;u+a6zl!UL|lPz#k% zRNs+h8)3(w?@8Xil?SiCo4CBcYf#pI_F1A0s$sBmjd{hr;FT7Y`l~&Cl~lrrP_Qmc z`awSSy4K1NwLlX*Ay+l;9L*P@9)Bobcn=q;4C)>-@Y&0*Pxe{>|;nUETeamT&k2#es&C~4_x!!&ESO&(Ocs@ht zE8^SgRPp#opr79V+eo9@+EAslao?_c<<7%Wij4RM39^%JcAI-+P983_OMj%?h>W;C z9-(0eLVp$0Hgkdt+ zIyJUln(lx6y}#(ZjB39B#dMKfqIJ{vTAuG3;ig|=y?<-LU!ddI_y1PARsH_gZk>O8 z|N9p7*Z^?sF`53i$ENXdx-a0QDR+Lb(?;x0UgVRu8?r-O2<~9|Ae7jSpX;i0RI_vC;$Ke diff --git a/apps/_infra/deploy-k8s/charts/opencrane-channel-proxy-0.8.0.tgz b/apps/_infra/deploy-k8s/charts/opencrane-channel-proxy-0.8.0.tgz index 3e72a76655691bdd7c2bbff985d39701052bc055..765048489c28633144cef5d07561cc22b032b2e6 100644 GIT binary patch delta 1658 zcmV-=28H?X5Bm?0Jb!I(;yU*4Yd(dQJ0snZh7g!Ccm9$7FSwAIL85?w&VyDfWRe4T z>e#-vL!0gGcV9UnKtgB>-MzEjt>2X5hu`l!*ykJ@E`a$GGpPDY%oxzBko+dDwU=0G zJH{bBdvbFe$9aE#p8Rzjr})=tG#h92X5+m6uKuo3Z=5;x=6`RE`WbSb4*hV8l*aPR zc`{Z}xM$L^AYVW#!kLR!jS3cG^RHw7pWUbgpnOS$PQGdhQRUYR0>p3#K~+O2DAs@| znMo{TH0KiOC7`u4AuS`N(Tqndz%sTgW}1r*I9)}@0^Dq^%4$pGgY8s)wx<3&+oAoR zSyt7i5)_Z54}Z$m;-u5y{{3HXo;&B+{&(t)=9$xQ-v9Qp|NoM9ds{^T%n1WzZG6FA z`@+z;g1xc7g{pUV6*Gg(k^KeJ2vmM3Fg&9Wp!Kj-)u!RJr z?7cC68bUat8vQCHVS0`%Vu*ksLh$fZsN&&7{=<%+PtMdNgM%lCQ2bR`6h1FW%IK`0FXx3}niis#|Ea+9Icaxeb^E^k{Uvpf|JiV z7wezyAekK3ll}rL4Tvq13G7Fb=(x?9h8vSM14AC}&b@!E#tU?JXB9Dw&WD^5KR&#> z(gnn3N zURrb%+N3r5Md}eXl(^nT664aMV5J0lj-l@C-?YdDSC)+ycID0d#sQ4 z!q&u__PQ6{BM%jcHS|bG9z0P>;J$YIox#`R{T*ut_)1v7uMd;|1QmacgCXxP>|+9VJKfs;IAi1pG%wH1~%8L`@CMlG443RALZ zwHKdE2VtPpQ1Th%*_Z(J33M?giWJ$*jQ>&WOXAjEWFw4df@udRj>q707AO}razg}2 z!h=m`bElJ%6`VxTOniR|!{&Vq>rPIM>q`Q6&#yH9jzfC*EKa5!@Rrn5O7geE4k9G~ z0ltPH&tzR#U+@52q6+c0cV(D~M-oa$%01|D5{;k?3<03s9y$ z>ZX^G)?1TV1~~yilamHAWg~QZf)mxxiwV^O-c-EHeY*Z1du^K${Utj+QKvsPc98H# zL?i@A^Bvs+f*b7XEDj>cBa;NhN E0Dgx+X8-^I delta 1689 zcmV;K24?yD5AY9=Jb&$P;y4=bXZ{K+yCdyLLkKLLts>nA7BVwP6cEsP(dmRt@&KMX zw%2xObKU;$Cnp3*2yLNvJJ-AVODTT&{hk;5c^(@sfcX+LsQOFH7|^Pa{5Gz&msn~$ z#vwg>c5@uZdEaa%e;vmu{&gDXjkEfBqgi*FP3KqV%&DLM)_-u$kn?=#$6KT{mS@hh zv5LaIkcI{M3{nx!T(oLbun?Pn9s9y|Dgh{85}}i?T0&I$4TAtN970gl5DJPlAWCKu z%NWhMM0yEm?Mz6^NNF_V5eu-4?TVS^q61D>(XjwGTdT6l6Zz4a`rmAa_J3wsRhvps zJd!>rTZ@xUgMWwjfBn4aH1qqv;k^Itb^re*?e4CM0+3;v zZ&nuig)pB+LC&P}h*ER=97F3hX`(K?&@`mSnn`28Nlw#0)P$+6IvLV3Rf1 zA(uK8JbxEI>XmE40vCxywS|TLfF^Q3?u_>+=thE9tzHYX&2?eaHM%?xaTFA@M~Z$ znq#VcQ&tA!vB57|0oF%$^zJ$%Z?39qd8h95xdwDJCYhgnSztgIT!1n?jV^QH3X5_BNJ)o1K z111q$tzOUiN3Kr-4hD?I##$c;li33;e-lCWHb8#$!O-isN3Fg$?e-_$sNd>MuSUIt zc;%_WBN**Zt$yF@O^2hw*KgBSuQ&MOb*6(+_hYv|J_z-l6NapJ)?Qk46xyUU`shuj zp9bT}bbR&UL-(uq1jtggYAhF^P3|;~=5uljM^YJi<3aDUH=0hmm)_uNGQB*^f8;>S zQk{)rqqkxY(; z6Vbb!p7%^GbOQ<}s^GHqb?SZg`jhGJ-zE?EWGQ-xZ@m>T{d=sB_QKZ0oA$bw-6Ibb zi8b^{NFF^=O5nb9`<=m;Lci4OcW`ynHm4H*q6kuy~suw(FD^De^4Bc!RIVc zE^6e42#|yao6hr{PD)mA5=ArdISkJqVpw-_YFu9uxO;x3`8OQW!)I|a?SQwWo>G#( z9d-~Q`FHR&1bHUw!upa2;3B8lbaK6=i z+~?FqllCzC?u5)X&6$SVom(_51AW8gHDL?;`Xb@HF?K)gN-KyLY;s|ap8uZv1(E1; z5(`kKKI*2Ik=8qtWCl3_LX(~bGJg|vdx8_yo5h6c5pOEqp2+%CvTC>~){~;$goz?WtA*Yw)?R|Zr zs#u8ilEEf*e83o2oqc1R|N3ORt%nv~QgX?*%{9>7_mtFT6QB@+ma)kZjFV3XBY(ba z^RT~bpw$iNv%S4URa=R;refFAcdjbSKmO=9xF6XLz5j+}k!GN(=Gb-HscK$L-r?=< zinHS|9{c@Ay-_c||DK;WUf+NJigs5)$TE3J@`ImsK54ngN~~_ZGu@i+cV1+jCdo8k jG0ufm`TyQbukE$Hw%7K8+P?w-01E&AV@^+A04e|gU_(`v diff --git a/apps/agent-controller/helm/Chart.yaml b/apps/agent-controller/helm/Chart.yaml index a4185d84c..67addca00 100644 --- a/apps/agent-controller/helm/Chart.yaml +++ b/apps/agent-controller/helm/Chart.yaml @@ -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" diff --git a/apps/agent-controller/package.json b/apps/agent-controller/package.json index 27f4e729b..f01ab5a04 100644 --- a/apps/agent-controller/package.json +++ b/apps/agent-controller/package.json @@ -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", diff --git a/apps/agent-controller/project.json b/apps/agent-controller/project.json index c0972aa71..f4badb9bb 100644 --- a/apps/agent-controller/project.json +++ b/apps/agent-controller/project.json @@ -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": { diff --git a/apps/artifact-preprocessor/helm/Chart.yaml b/apps/artifact-preprocessor/helm/Chart.yaml index 8ead21564..35f2de7c0 100644 --- a/apps/artifact-preprocessor/helm/Chart.yaml +++ b/apps/artifact-preprocessor/helm/Chart.yaml @@ -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" diff --git a/apps/artifact-preprocessor/package.json b/apps/artifact-preprocessor/package.json index e5441e830..9b52dffa8 100644 --- a/apps/artifact-preprocessor/package.json +++ b/apps/artifact-preprocessor/package.json @@ -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", diff --git a/apps/artifact-preprocessor/project.json b/apps/artifact-preprocessor/project.json index 53cc87653..2c43e76c3 100644 --- a/apps/artifact-preprocessor/project.json +++ b/apps/artifact-preprocessor/project.json @@ -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": { diff --git a/apps/artifact-scanner/helm/Chart.yaml b/apps/artifact-scanner/helm/Chart.yaml index ed6602adc..10ed5e8c4 100644 --- a/apps/artifact-scanner/helm/Chart.yaml +++ b/apps/artifact-scanner/helm/Chart.yaml @@ -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" diff --git a/apps/artifact-scanner/package.json b/apps/artifact-scanner/package.json index e9dc81c95..a534e2e93 100644 --- a/apps/artifact-scanner/package.json +++ b/apps/artifact-scanner/package.json @@ -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", diff --git a/apps/artifact-scanner/project.json b/apps/artifact-scanner/project.json index 9ef0a3590..889ced90a 100644 --- a/apps/artifact-scanner/project.json +++ b/apps/artifact-scanner/project.json @@ -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": { diff --git a/apps/artifact-service/helm/Chart.yaml b/apps/artifact-service/helm/Chart.yaml index 02be92ea8..4e58167e7 100644 --- a/apps/artifact-service/helm/Chart.yaml +++ b/apps/artifact-service/helm/Chart.yaml @@ -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" diff --git a/apps/artifact-service/package.json b/apps/artifact-service/package.json index 31bf778bc..75835c186 100644 --- a/apps/artifact-service/package.json +++ b/apps/artifact-service/package.json @@ -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", diff --git a/apps/artifact-service/project.json b/apps/artifact-service/project.json index cbd580ec8..86df09dac 100644 --- a/apps/artifact-service/project.json +++ b/apps/artifact-service/project.json @@ -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": { diff --git a/apps/channel-proxy/helm/Chart.yaml b/apps/channel-proxy/helm/Chart.yaml index c2fb48a70..6ee1dfc96 100644 --- a/apps/channel-proxy/helm/Chart.yaml +++ b/apps/channel-proxy/helm/Chart.yaml @@ -3,4 +3,4 @@ name: opencrane-channel-proxy description: App-owned named-template library for the channel trust boundary. type: library version: 0.8.0 -appVersion: "0.8.0" +appVersion: "0.9.2" diff --git a/apps/channel-proxy/package.json b/apps/channel-proxy/package.json index 90a30ca9f..490273d91 100644 --- a/apps/channel-proxy/package.json +++ b/apps/channel-proxy/package.json @@ -1,6 +1,6 @@ { "name": "@opencrane/channel-proxy", - "version": "0.8.0", + "version": "0.9.2", "private": true, "license": "AGPL-3.0-or-later", "type": "module", diff --git a/apps/channel-proxy/project.json b/apps/channel-proxy/project.json index b5e2ad1b9..b43cd0bdc 100644 --- a/apps/channel-proxy/project.json +++ b/apps/channel-proxy/project.json @@ -2,7 +2,7 @@ "name": "channel-proxy", "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "application", - "metadata": { "release": { "adaptedVersion": "0.8.0" } }, + "metadata": { "release": { "adaptedVersion": "0.9.2" } }, "sourceRoot": "apps/channel-proxy/src", "tags": ["type:app", "layer:entrypoint", "scope:app"], "targets": { diff --git a/docs/agents/versioning.md b/docs/agents/versioning.md index 38bf2717e..0041a2fb3 100644 --- a/docs/agents/versioning.md +++ b/docs/agents/versioning.md @@ -5,11 +5,12 @@ ## Version authorities The root [`package.json`](../../package.json) version names the current repository train. Every Nx -application records `metadata.release.adaptedVersion`: the exact repository version in which that -application's production or deployment contract was last adapted, either directly or through a -changed project in its Nx dependency graph. Unchanged applications -retain their older value. Existing app `package.json` versions and app-owned Helm chart versions are -checked mirrors, never competing authorities. +application records `metadata.release.adaptedVersion`: the exact repository version in which files +under that application's own root last changed. An application whose own files did not change keeps +its latest released value, even when a shared library, the root dependency set, or the lockfile +moved underneath it — published images are pinned by commit SHA, so those shared changes reach the +application without a per-application stamp. Existing app `package.json` versions and app-owned +Helm chart versions are checked mirrors, never competing authorities. The initial `adoptionBaseline` is the explicit exception: it stamps the observed fleet composition at the train where this ledger was introduced, because older per-app adaptation history was not @@ -18,10 +19,10 @@ immutable. Once a repository-version tag exists, production or deployment compos change under that same version. [`releases/`](../../releases/README.md) is the immutable composition history. Each root version maps -the app, chart, and database schema versions known to work together. Use the Nx project graph to -start from directly changed project roots and stamp the applications that own those roots or depend -on them. Do not equate Nx's broader `affected` result with adaptation: named inputs such as shared -configuration can mark an app affected without changing its production or deployment contract. +the app, chart, and database schema versions known to work together. Stamp exactly the applications +that own a changed file under their project root. Do not equate Nx's `affected` result with +adaptation: a dependency-graph or shared-configuration change can mark an app affected without +requiring a new stamp. This follows Nx's independent-release principle—projects retain their own last meaningful version— without enabling `nx release` as a second version authority. Nx @@ -46,13 +47,13 @@ manifest contract; do not introduce parallel version files. predecessor database identity and may carry only that predecessor's exact source. This reuses the already reviewed SQL identity; it does not create a skipped-version migration or permit multi-hop carry-forward. -- A directly changed application, and an application depending on a changed project, is stamped to - the current full root version. This is a compatibility stamp, not a claim that every application - releases in lockstep. -- A semantic root dependency or lockfile change stamps every application because the shared runtime - boundary cannot be mapped safely to a smaller owner set. Root/version-only mirror edits do not. +- A directly changed application is stamped to the current full root version. This is a + compatibility stamp, not a claim that every application releases in lockstep. +- Shared library, root dependency, and lockfile changes stamp nothing on their own: the affected + applications keep their latest released version and receive the shared change through their + SHA-pinned images. - Never rewrite an older release manifest. Create the next manifest by carrying unchanged component - versions forward and updating only directly or dependency-adapted owners. + versions forward and updating only directly changed owners. ## Helm migrations diff --git a/releases/0.9.2.json b/releases/0.9.2.json index 8e07ee915..495c266f4 100644 --- a/releases/0.9.2.json +++ b/releases/0.9.2.json @@ -20,10 +20,10 @@ }, "agent-controller": { "root": "apps/agent-controller", - "adaptedVersion": "0.9.2", - "packageVersion": "0.9.2", + "adaptedVersion": "0.9.1", + "packageVersion": "0.9.1", "chartVersion": "0.9.2", - "chartAppVersion": "0.9.2" + "chartAppVersion": "0.9.1" }, "agent-runtime": { "root": "apps/agent-runtime", @@ -31,31 +31,31 @@ }, "artifact-preprocessor": { "root": "apps/artifact-preprocessor", - "adaptedVersion": "0.9.2", - "packageVersion": "0.9.2", + "adaptedVersion": "0.9.1", + "packageVersion": "0.9.1", "chartVersion": "0.9.2", - "chartAppVersion": "0.9.2" + "chartAppVersion": "0.9.1" }, "artifact-scanner": { "root": "apps/artifact-scanner", - "adaptedVersion": "0.9.2", - "packageVersion": "0.9.2", + "adaptedVersion": "0.9.1", + "packageVersion": "0.9.1", "chartVersion": "0.9.2", - "chartAppVersion": "0.9.2" + "chartAppVersion": "0.9.1" }, "artifact-service": { "root": "apps/artifact-service", - "adaptedVersion": "0.8.0", - "packageVersion": "0.8.0", + "adaptedVersion": "0.9.2", + "packageVersion": "0.9.2", "chartVersion": "0.8.0", - "chartAppVersion": "0.8.0" + "chartAppVersion": "0.9.2" }, "channel-proxy": { "root": "apps/channel-proxy", - "adaptedVersion": "0.8.0", - "packageVersion": "0.8.0", + "adaptedVersion": "0.9.2", + "packageVersion": "0.9.2", "chartVersion": "0.8.0", - "chartAppVersion": "0.8.0" + "chartAppVersion": "0.9.2" }, "cognee": { "root": "apps/_infra/cognee", diff --git a/scripts/__tests__/release-versioning-check.test.mjs b/scripts/__tests__/release-versioning-check.test.mjs index 35568e66e..5736b867b 100644 --- a/scripts/__tests__/release-versioning-check.test.mjs +++ b/scripts/__tests__/release-versioning-check.test.mjs @@ -203,17 +203,17 @@ test("rejects a directly adapted project that retains an older stamp", async () { const fixture = _Fixture({ adaptedVersion: "0.6.2" }); const errors = await validateWorkspace(fixture.root, ["apps/example/src/index.ts"], fixture.graph); - assert.ok(errors.some((error) => error.includes("direct or dependency change"))); + assert.ok(errors.some((error) => error.includes("changed directly"))); }); test("counts test changes as direct application adaptations", async () => { const fixture = _Fixture({ adaptedVersion: "0.6.2" }); const errors = await validateWorkspace(fixture.root, ["apps/example/src/example.test.ts"], fixture.graph); - assert.ok(errors.some((error) => error.includes("direct or dependency change"))); + assert.ok(errors.some((error) => error.includes("changed directly"))); }); -test("uses the Nx dependency graph to stamp apps adapted through a changed library", async () => +test("keeps an application's stamp when only a depended-on library changed", async () => { const fixture = _Fixture({ adaptedVersion: "0.6.2" }); fixture.graph.nodes.contracts = { @@ -223,8 +223,7 @@ test("uses the Nx dependency graph to stamp apps adapted through a changed libra example: [{ source: "example", target: "contracts", type: "static" }], contracts: [], }; - const errors = await validateWorkspace(fixture.root, ["libs/contracts/src/index.ts"], fixture.graph); - assert.ok(errors.some((error) => error.includes("direct or dependency change"))); + assert.deepEqual(await validateWorkspace(fixture.root, ["libs/contracts/src/index.ts"], fixture.graph), []); }); test("rejects advancing an unaffected application's last-adapted version", async () => @@ -261,7 +260,7 @@ test("counts package and project configuration as direct adaptations", async () for (const file of ["apps/example/package.json", "apps/example/project.json"]) { const errors = await validateWorkspace(fixture.root, [file], fixture.graph); - assert.ok(errors.some((error) => error.includes("direct or dependency change"))); + assert.ok(errors.some((error) => error.includes("changed directly"))); } }); @@ -272,11 +271,10 @@ test("permits package and project stamp-only mirrors", async () => assert.deepEqual(await validateWorkspace(fixture.root, files, fixture.graph, files), []); }); -test("treats a semantic root dependency change as an adaptation of every application", async () => +test("keeps every application's stamp when only the root dependency set changed", async () => { const fixture = _Fixture({ adaptedVersion: "0.6.2" }); - const errors = await validateWorkspace(fixture.root, ["package.json"], fixture.graph); - assert.ok(errors.some((error) => error.includes("direct or dependency change"))); + assert.deepEqual(await validateWorkspace(fixture.root, ["package.json"], fixture.graph), []); }); test("permits root package and lockfile version-only mirrors", async () => diff --git a/scripts/release-versioning/core.mjs b/scripts/release-versioning/core.mjs index 6df13e942..236504799 100644 --- a/scripts/release-versioning/core.mjs +++ b/scripts/release-versioning/core.mjs @@ -94,47 +94,28 @@ function _ValidateTransition(manifest, errors) } } -function _AdaptedApplications(graph, changedFiles, stampOnlyFiles) +/** + * Selects the applications whose own files changed in this release. + * + * Only these applications must advance to the root version. An application that changed only + * through a shared library, the root dependency set, or the lockfile keeps its last released + * version: images are pinned by commit SHA at publication, so restating the root version on + * every untouched application recorded no extra information and failed CI on every shared + * change until each manifest entry was bumped by hand. + */ +function _DirectlyChangedApplications(graph, changedFiles, stampOnlyFiles) { const projectRoots = Object.entries(graph.nodes) .map(([name, node]) => [name, node.data.root]) .sort((left, right) => right[1].length - left[1].length); const touchedProjects = new Set(); - const rootRuntimeChanged = changedFiles.some((file) => - (file === "package.json" || file === "package-lock.json") && !stampOnlyFiles.has(file)); - if (rootRuntimeChanged) - { - for (const [name, node] of Object.entries(graph.nodes)) - if (node.data.projectType === "application") touchedProjects.add(name); - } for (const file of changedFiles) { if (stampOnlyFiles.has(file) || _IGNORED_TOUCH.some((pattern) => pattern.test(file))) continue; const owner = projectRoots.find(([, root]) => file === root || file.startsWith(`${root}/`)); if (owner) touchedProjects.add(owner[0]); } - const dependants = new Map(); - for (const [consumer, dependencies] of Object.entries(graph.dependencies ?? {})) - { - for (const dependency of dependencies) - { - const current = dependants.get(dependency.target) ?? []; - current.push(consumer); - dependants.set(dependency.target, current); - } - } - const affected = new Set(touchedProjects); - const queue = [...touchedProjects]; - while (queue.length > 0) - { - for (const dependant of dependants.get(queue.shift()) ?? []) - { - if (affected.has(dependant)) continue; - affected.add(dependant); - queue.push(dependant); - } - } - return new Set([...affected].filter((name) => graph.nodes[name]?.data.projectType === "application")); + return new Set([...touchedProjects].filter((name) => graph.nodes[name]?.data.projectType === "application")); } function _ValidateProject( @@ -175,7 +156,7 @@ function _ValidateProject( if (previousProject && compareSemver(project.adaptedVersion, previousProject.adaptedVersion) < 0) errors.push(`${name} adapted version regresses from '${previousProject.adaptedVersion}' to '${project.adaptedVersion}'`); if (directlyTouched && project.adaptedVersion !== rootVersion) - errors.push(`${name} is adapted by a direct or dependency change but remains at '${project.adaptedVersion}' instead of root '${rootVersion}'`); + errors.push(`${name} changed directly but remains at '${project.adaptedVersion}' instead of root '${rootVersion}'`); if (previousProject && !directlyTouched && project.adaptedVersion !== previousProject.adaptedVersion) errors.push(`${name} was not adapted but its version changed from '${previousProject.adaptedVersion}' to '${project.adaptedVersion}'`); const chartTouched = changedFiles.some((file) => file.startsWith(`${project.root}/helm/`) @@ -281,7 +262,7 @@ export async function validateWorkspace( if (previousManifest) errors.push(...validateReleaseManifest(previousManifest, "previous release manifest")); } const graph = suppliedGraph ?? await createProjectGraphAsync(); - const adaptedApplications = _AdaptedApplications(graph, changedFiles, stampOnlyFiles); + const adaptedApplications = _DirectlyChangedApplications(graph, changedFiles, stampOnlyFiles); const applicationNames = Object.entries(graph.nodes) .filter(([, node]) => node.data.projectType === "application") .map(([name]) => name) From 0e525317447527f4ae485f5e2cc8141fd3fb746c Mon Sep 17 00:00:00 2001 From: Jente Rosseel Date: Wed, 19 Aug 2026 14:51:05 +0300 Subject: [PATCH 4/9] =?UTF-8?q?=E2=9A=A1=20split=20Storybook=20from=20the?= =?UTF-8?q?=20critical=20path=20and=20stop=20hanging=20on=20apt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Chromium install and Storybook chain ran serially inside the test job and its apt phase has hung an untimed job for a long stretch, holding a runner and starving the queue. Storybook visual regressions now run as their own parallel job with cached browser binaries (the apt install only happens on a cold cache and is capped at ten minutes), every job carries a timeout, ripgrep installs as a pinned static binary instead of a full apt round-trip, and the layer cache splits into a trusted and a pull-request repository so unreviewed layers can never reach a published image. --- .github/workflows/docker.yml | 131 +++++++++++++++--- .../platform/database-release-finalization.sh | 2 +- .../platform/tests/develop-smoke.sh | 18 ++- 3 files changed, 120 insertions(+), 31 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2bfe7d380..b20b2ab7d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -45,15 +45,22 @@ permissions: env: REGISTRY: ghcr.io - # Image layer cache lives in one registry repository with one tag per deployable. The registry + # Image layer cache lives in registry repositories with one tag per deployable. The registry # backend replaced the GitHub Actions cache backend because the 10GB Actions cache quota evicted # the large layer blobs on almost every run, so every build started cold. + # + # Two repositories form a trust boundary: only integration-branch pushes write the trusted + # cache, and published image builds read only the trusted cache. Same-repository pull requests + # read both but write only the pull-request cache, so a layer produced by unreviewed code can + # never become part of a published image. BUILD_CACHE_IMAGE: opencrane-buildcache + BUILD_CACHE_IMAGE_PR: opencrane-buildcache-pr jobs: prepare: name: Calculate affected work runs-on: ubuntu-latest + timeout-minutes: 15 outputs: nx_base: ${{ steps.affected.outputs.nx_base }} nx_head: ${{ steps.affected.outputs.nx_head }} @@ -130,6 +137,9 @@ jobs: test: name: Build, test, and lint affected projects runs-on: ubuntu-latest + # A hung step must fail fast: an untimed job once held a runner for hours on a stuck + # apt install and starved the whole queue. + timeout-minutes: 45 needs: prepare services: postgres: @@ -193,10 +203,21 @@ jobs: terraform init -backend=false -input=false -lockfile=readonly terraform validate -no-color + # The runner image already ships the PostgreSQL client. ripgrep comes as a pinned static + # binary because a full apt update/install spent ~30s per run and can hang on apt locks. - name: Install validation clients + env: + RIPGREP_VERSION: 14.1.1 + RIPGREP_SHA256: 4cf9f2741e6c465ffdb7c26f38056a59e2a2544b51f7cc128ef28337eeae4d8e run: | - sudo apt-get update - sudo apt-get install --yes postgresql-client ripgrep + if ! command -v psql >/dev/null; then + sudo apt-get update && sudo apt-get install --yes --no-install-recommends postgresql-client + fi + curl --location --fail --silent --show-error --output /tmp/ripgrep.tar.gz \ + "https://github.com/BurntSushi/ripgrep/releases/download/${RIPGREP_VERSION}/ripgrep-${RIPGREP_VERSION}-x86_64-unknown-linux-musl.tar.gz" + echo "${RIPGREP_SHA256} /tmp/ripgrep.tar.gz" | sha256sum --check + tar --extract --gzip --file /tmp/ripgrep.tar.gz --directory /tmp + sudo install --mode 0755 "/tmp/ripgrep-${RIPGREP_VERSION}-x86_64-unknown-linux-musl/rg" /usr/local/bin/rg - name: Enforce workload ownership and app composition run: npm run check:workload-ownership-app-composition @@ -281,6 +302,50 @@ jobs: - name: Build, test, and lint affected projects run: npx nx affected -t build test lint + - name: Enforce monorepo dependency boundaries + run: npm run lint:boundaries + + - name: Verify API reference and generated client when affected + if: needs.prepare.outputs.api_contract_changed == 'true' + run: | + npx nx run opencrane:build + npm run sync-openapi -w @opencrane/website + git diff --exit-code -- website/public/openapi.json + npx nx run contracts:generate + git diff --exit-code -- libs/contracts/src/generated/api.ts + + # Storybook visual regressions run beside the main test job instead of serially after it: + # the Chromium install and the Storybook build/test chain used to extend the critical path + # by several minutes whenever a frontend project was affected. + storybook_visual: + name: Storybook component contracts + runs-on: ubuntu-latest + timeout-minutes: 30 + needs: prepare + env: + NX_BASE: ${{ needs.prepare.outputs.nx_base }} + NX_HEAD: ${{ needs.prepare.outputs.nx_head }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + + - name: Restore node_modules + id: node-modules + uses: actions/cache@v4 + with: + path: node_modules + key: node-modules-${{ runner.os }}-node24-${{ hashFiles('package-lock.json') }} + + - name: Install dependencies + if: steps.node-modules.outputs.cache-hit != 'true' + run: npm ci + - name: Detect affected Storybook regression coverage id: storybook-regression shell: bash @@ -292,8 +357,30 @@ jobs: echo "required=false" >> "$GITHUB_OUTPUT" fi - - name: Install Chromium for affected Storybook regressions + - name: Restore the Nx computation cache if: steps.storybook-regression.outputs.required == 'true' + uses: actions/cache@v4 + with: + path: .nx/cache + key: nx-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-${{ github.sha }} + restore-keys: | + nx-${{ runner.os }}-${{ hashFiles('package-lock.json') }}- + + # The browser binaries change only with the lockfile, so they restore from cache instead + # of downloading each run. On a cache hit the apt-driven install-deps is skipped entirely: + # the runner image already carries Chromium's system libraries, and the apt step has hung + # for tens of minutes on runner apt locks. + - name: Restore the Playwright browser cache + if: steps.storybook-regression.outputs.required == 'true' + id: playwright-browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + + - name: Install Chromium for affected Storybook regressions + if: steps.storybook-regression.outputs.required == 'true' && steps.playwright-browsers.outputs.cache-hit != 'true' + timeout-minutes: 10 run: npx playwright install --with-deps chromium - name: Build and test affected Storybook component contracts @@ -310,18 +397,6 @@ jobs: if-no-files-found: ignore retention-days: 7 - - name: Enforce monorepo dependency boundaries - run: npm run lint:boundaries - - - name: Verify API reference and generated client when affected - if: needs.prepare.outputs.api_contract_changed == 'true' - run: | - npx nx run opencrane:build - npm run sync-openapi -w @opencrane/website - git diff --exit-code -- website/public/openapi.json - npx nx run contracts:generate - git diff --exit-code -- libs/contracts/src/generated/api.ts - develop_smoke: name: k3d current-silo smoke test runs-on: ubuntu-latest @@ -401,8 +476,10 @@ jobs: SMOKE_REGISTRY: ${{ env.REGISTRY }}/${{ github.repository_owner }} SMOKE_STORAGE_MODE: ${{ needs.prepare.outputs.develop_smoke_storage_mode }} SMOKE_BUILD_CACHE: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.BUILD_CACHE_IMAGE }} - # Fork pull requests get a read-only token, so they consume the cache without exporting. - SMOKE_BUILD_CACHE_PUSH: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && '1' || '0' }} + SMOKE_BUILD_CACHE_UNTRUSTED: ${{ github.event_name == 'pull_request' && format('{0}/{1}/{2}', env.REGISTRY, github.repository_owner, env.BUILD_CACHE_IMAGE_PR) || '' }} + # Integration pushes refresh the trusted cache; same-repository pull requests export + # only the pull-request cache; fork pull requests (read-only token) export nothing. + SMOKE_BUILD_CACHE_EXPORT: ${{ github.event_name == 'push' && format('{0}/{1}/{2}', env.REGISTRY, github.repository_owner, env.BUILD_CACHE_IMAGE) || (github.event.pull_request.head.repo.full_name == github.repository && format('{0}/{1}/{2}', env.REGISTRY, github.repository_owner, env.BUILD_CACHE_IMAGE_PR) || '') }} image_smoke: name: Image smoke (${{ matrix.project }}) @@ -440,12 +517,14 @@ jobs: build-and-push: name: Build and publish affected images runs-on: ubuntu-latest - needs: [prepare, test, develop_smoke, image_smoke] + timeout-minutes: 30 + needs: [prepare, test, storybook_visual, develop_smoke, image_smoke] if: >- ${{ always() && needs.prepare.outputs.has_deployables == 'true' && needs.test.result == 'success' && + needs.storybook_visual.result == 'success' && (needs.develop_smoke.result == 'success' || needs.develop_smoke.result == 'skipped') && (needs.image_smoke.result == 'success' || needs.image_smoke.result == 'skipped') }} @@ -487,20 +566,26 @@ jobs: push: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_deployables != 'none') }} tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} - cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.BUILD_CACHE_IMAGE }}:${{ matrix.project }} - # Fork pull requests get a read-only token, so they consume the cache without exporting. - cache-to: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && format('type=registry,ref={0}/{1}/{2}:{3},mode=max', env.REGISTRY, github.repository_owner, env.BUILD_CACHE_IMAGE, matrix.project) || '' }} + # Publishable builds (push events) read only the trusted cache. Pull-request + # validation builds also read the pull-request cache and export only there; fork + # pull requests (read-only token) export nothing. + cache-from: | + type=registry,ref=${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.BUILD_CACHE_IMAGE }}:${{ matrix.project }} + ${{ github.event_name == 'pull_request' && format('type=registry,ref={0}/{1}/{2}:{3}', env.REGISTRY, github.repository_owner, env.BUILD_CACHE_IMAGE_PR, matrix.project) || '' }} + cache-to: ${{ github.event_name == 'push' && format('type=registry,ref={0}/{1}/{2}:{3},mode=max', env.REGISTRY, github.repository_owner, env.BUILD_CACHE_IMAGE, matrix.project) || (github.event.pull_request.head.repo.full_name == github.repository && format('type=registry,ref={0}/{1}/{2}:{3},mode=max', env.REGISTRY, github.repository_owner, env.BUILD_CACHE_IMAGE_PR, matrix.project) || '') }} publish-develop-smoke-images: name: Publish develop smoke image (${{ matrix.project }}) runs-on: ubuntu-latest - needs: [prepare, test, develop_smoke, image_smoke, build-and-push] + timeout-minutes: 20 + needs: [prepare, test, storybook_visual, develop_smoke, image_smoke, build-and-push] if: >- ${{ always() && github.event_name == 'push' && github.ref == 'refs/heads/develop' && needs.test.result == 'success' && + needs.storybook_visual.result == 'success' && needs.develop_smoke.result == 'success' && (needs.image_smoke.result == 'success' || needs.image_smoke.result == 'skipped') && (needs['build-and-push'].result == 'success' || needs['build-and-push'].result == 'skipped') diff --git a/apps/_infra/deploy-k8s/platform/database-release-finalization.sh b/apps/_infra/deploy-k8s/platform/database-release-finalization.sh index ee0351579..79d694bb8 100755 --- a/apps/_infra/deploy-k8s/platform/database-release-finalization.sh +++ b/apps/_infra/deploy-k8s/platform/database-release-finalization.sh @@ -76,7 +76,7 @@ compute_database_connection_checksum() shift kubectl get secret "$@" -n "$namespace" \ -o jsonpath='{range .items[*]}{.metadata.name}{":"}{.data}{"\n"}{end}' \ - | sha256sum | cut -d' ' -f1 + | LC_ALL=C sort | sha256sum | cut -d' ' -f1 } # Stamps the connection-Secret checksum onto each consumer Deployment's pod template. An diff --git a/apps/_infra/deploy-k8s/platform/tests/develop-smoke.sh b/apps/_infra/deploy-k8s/platform/tests/develop-smoke.sh index 30e50c385..7428a5c5a 100755 --- a/apps/_infra/deploy-k8s/platform/tests/develop-smoke.sh +++ b/apps/_infra/deploy-k8s/platform/tests/develop-smoke.sh @@ -153,15 +153,19 @@ _build_image() local image="$2" local dockerfile="$3" local cache_arguments=() - # CI shares one registry layer cache per deployable with the publish jobs (see - # BUILD_CACHE_IMAGE in docker.yml). The smoke also exports its layers when the token can - # write, so the next pull-request push builds warm instead of cold. Local runs leave - # SMOKE_BUILD_CACHE unset and build without a remote cache. + # 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}") - if [[ "${SMOKE_BUILD_CACHE_PUSH:-0}" == "1" ]]; then - cache_arguments+=(--cache-to "type=registry,ref=${SMOKE_BUILD_CACHE}:${project},mode=max") - fi + 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" \ From 0b661dee7e22e146892a6cf00440b3da89635bd3 Mon Sep 17 00:00:00 2001 From: Jente Rosseel Date: Wed, 19 Aug 2026 14:56:51 +0300 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=90=9B=20align=20the=20affected-deplo?= =?UTF-8?q?yables=20contract=20with=20the=20registry=20cache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📝 document CI, deploys, warnings, and version migrations in docs/ci-and-deploy.md --- docs/README.md | 1 + docs/ci-and-deploy.md | 210 ++++++++++++++++++ .../__tests__/affected-deployables.test.mjs | 15 +- 3 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 docs/ci-and-deploy.md diff --git a/docs/README.md b/docs/README.md index 0f40cfcff..8831baaf2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,6 +9,7 @@ belong in `CHANGELOG.md`, while only lasting design contracts remain under `docs | Folder | What it holds | |--------|---------------| +| [`ci-and-deploy.md`](ci-and-deploy.md) | The CI pipeline, its caching layers, the deploy engine, operational warnings, and the release-version and migration process. | | [`agents/`](agents/) | Canonical agent guidance, indexed by [`AGENTS.md`](../AGENTS.md). | | [`adr/`](adr/) | Accepted architecture decisions and the exact clauses they supersede. | | [`design/`](design/) | Durable product and platform contracts grounded in current implementations. | diff --git a/docs/ci-and-deploy.md b/docs/ci-and-deploy.md new file mode 100644 index 000000000..40bc0708b --- /dev/null +++ b/docs/ci-and-deploy.md @@ -0,0 +1,210 @@ +# CI and deployment + +This is the reference for how a change travels from a pull request to a running cluster: the CI +pipeline and its gates, the caching that keeps it fast, the deploy engine, the warnings that save +hours, and the release-version and migration process. The contributor-facing summary lives on the +website under Contributing; this file is the deeper repository-side reference. + +> See also: [`docs/agents/versioning.md`](agents/versioning.md) for the full release-version +> policy, [`docs/agents/infra.md`](agents/infra.md) for build/infra rules, and +> [`docs/agents/deploy-ledger.md`](agents/deploy-ledger.md) for the deploy fleet's cross-run notes. + +## The pipeline at a glance + +Three workflows run on every pull request: **Validate and publish affected deployables** +(`.github/workflows/docker.yml`, the main pipeline), **Enforce pull-request stack integrity** +(stacked-PR bookkeeping, under a minute), and **CodeQL** (static analysis, a few minutes). The +main pipeline is where the time goes: + +```mermaid +flowchart LR + P[prepare\ncalculate affected work\n~1-2 min] --> T[test\nbuild, test, lint,\npolicy guards\n~3-12 min] + P --> S[storybook_visual\ncomponent contracts\nskips fast when\nnothing affected] + P --> K[develop_smoke\nk3d current-silo smoke\n~6-15 min] + P --> I[image_smoke\nper-image boot checks\n~1-2 min each] + T --> B[build-and-push\npublish affected images\npush events only] + S --> B + K --> B + I --> B + B --> D[publish-develop-smoke-images\ncomplete the immutable\ndevelop image set] + K --> D +``` + +What each job owns: + +| Job | Purpose | Typical duration | +| --- | --- | --- | +| `prepare` | Computes the Nx affected graph, the deployable matrix, the guard comparison base, and whether the k3d smoke can be skipped. | 1–2 min | +| `test` | Builds, tests, and lints affected projects, and runs every policy guard: workload ownership, agent-domain boundary, mechanical style, module growth, release versioning, database migration contracts, Prisma boundaries, config-docs coverage, dependency boundaries. | 3–12 min | +| `storybook_visual` | Storybook build/behaviour/visual contracts for affected frontend projects, on cached Chromium. Runs beside `test`, not after it. | seconds when nothing affected; ~5 min otherwise | +| `develop_smoke` | Boots a disposable k3d cluster, deploys the full current silo through the real deploy scripts, and proves database isolation, TLS ingress, and workload health. The long pole of the pipeline. | 6–15 min | +| `image_smoke` | Boots individual images that declare an `image-smoke` target and checks they come up. | 1–2 min per image | +| `build-and-push` | Builds every affected deployable image and publishes `sha-` tags on push events. On pull requests it builds without pushing, as a proof. | 1–5 min warm | +| `publish-develop-smoke-images` | On develop pushes, completes the immutable image set for the commit: reuses the exact validated base image where nothing changed, copies tags forward, builds only what is missing. | seconds per image | + +A deploy to a live cluster then consumes the published `sha-` images — CI must be green +for the exact SHA before any deploy (the deploy scripts do not build images). + +## Caching layers + +Every job runs on a fresh runner, so anything not cached is paid on every run. The pipeline uses +these caches, from cheapest to most impactful: + +| Cache | Backend | Key | Used by | +| --- | --- | --- | --- | +| npm download cache | `actions/setup-node` | lockfile hash | all jobs | +| `node_modules` | `actions/cache` | lockfile hash | all jobs (skips `npm ci` entirely on a hit) | +| Nx computation cache | `actions/cache` (`.nx/cache`) | lockfile hash + commit, with prefix restore | `test`, `storybook_visual` | +| Playwright Chromium | `actions/cache` (`~/.cache/ms-playwright`) | lockfile hash | `storybook_visual` | +| Docker image layers | **registry** (`ghcr.io//opencrane-buildcache:`) | buildx layer graph | `develop_smoke`, `build-and-push`, `publish-develop-smoke-images` | +| npm inside Dockerfiles | BuildKit cache mount (`/root/.npm`) | shared between build and runtime stages within one build | all Node images | + +Design decisions worth knowing: + +- **Image layers cache in the registry, not in the Actions cache.** The repository's 10GB Actions + cache quota was permanently over budget — single buildkit blobs reach ~700MB — so layer caches + were evicted almost immediately and every image built cold. Registry-backed caches + (`type=registry`) have no such quota, and moving them out also stops the node/Nx caches from + being evicted alongside. +- **The layer cache has a trust boundary.** Two cache repositories exist: + `opencrane-buildcache` (trusted) is written only by integration-branch pushes and is the only + cache a publishable build reads; `opencrane-buildcache-pr` is written and read by + same-repository pull-request validation builds. A layer produced by unreviewed code therefore + never becomes part of a published image. Fork pull requests read the trusted cache and write + nothing (their token is read-only). +- **Chromium installs from cache, and the apt step is capped.** The browser binaries restore from + the Actions cache; the apt-driven `--with-deps` install runs only on a cold cache and carries a + ten-minute step timeout, because a hung apt once held an untimed job (and its runner) for hours. +- **Every job has a `timeout-minutes`.** A hung job does not only lose its own time — it occupies + a concurrent-runner slot and starves every queued run behind it. + +## The k3d smoke and its skip proof + +The `develop_smoke` job is the pipeline's long pole and its most valuable gate: it exercises the +real deploy path end to end on every develop push. Two mechanisms keep its cost down: + +1. **Image reuse by digest.** Projects the affected graph did not select are pulled from the + validated base commit's published images instead of being rebuilt + (`develop-smoke.sh:_pull_baseline_image`). +2. **The skip proof.** A pull request skips the k3d smoke entirely when its exact base SHA + already completed the same k3d job successfully (`scripts/develop-smoke-baseline.mjs`). Any + ambiguity — API failure, missing proof, affected containers — deliberately runs the smoke. + +The skip proof is why **keeping develop green is a speed feature**: a red develop push means no +validated base exists, so every subsequent pull request pays the full smoke. A failure streak on +develop taxes every open PR in the repository. + +The smoke's storage tier also varies: touching `k8s-deploy.sh`, the smoke script, the workflow, +or `apps/postgres/` selects the `full` tier, which additionally exercises CSI volume expansion +(several extra minutes). Other changes run the `fast` tier. + +## Deploy infrastructure + +All cluster mutation goes through app-owned scripts — never bare `helm upgrade` or `kubectl +apply` against a live cluster: + +```mermaid +flowchart TD + A[apps/_infra/deploy-k8s/deploy.sh\nsilo profile: flags, presets] --> B[platform/k8s-deploy.sh\nthe install engine] + B --> C[current-chart-sources.sh\nhelm dependency build\nfrom Chart.lock] + B --> D[database-migration-orchestrator.sh\nCNPG cluster, databases,\nmigration + privileges Jobs] + B --> E[umbrella helm upgrade\nall app subcharts] + E --> F[database-release-finalization.sh\ncredential-checksum roll,\nrollout waits, cert wait] + F --> G[post-deploy-verify.sh\nlive health verification] +``` + +- `deploy.sh` installs one per-ClusterTenant silo: operator, channel proxy, LiteLLM, Cognee, + opencrane-ui, per-CT networking, and one app-owned PostgreSQL server with isolated logical + databases. Required flags: `--base-domain`, `--cluster-tenant`, `--acme-email`, + `--first-user-email`; fresh installs also need `--opencrane-ui-digest` and `--cognee-digest` + (immutable digests, never tags). +- Cluster-wide prerequisites (ingress-nginx, cert-manager, CloudNativePG) are installed once per + cluster by `bootstrap-prerequisites.sh` and are never part of a silo release. +- The PostgreSQL transition is resolved and schema-validated *before* the cluster is touched; + migration and privileges run as bounded Helm hook Jobs, with an automatic rollback path that + restores the exact fenced Helm revision on failure. +- After the umbrella upgrade, the engine stamps a checksum of the published database connection + Secrets onto the consumer Deployments (`opencrane-server`, `litellm`, `mcp-gateway`). An + unchanged checksum is a no-op; a changed one triggers exactly one rollout. (This replaced an + unconditional `rollout restart` that double-started the heaviest workloads on every deploy.) + +## Warnings — read before deploying + +- **CI green first.** Confirm the `docker.yml` run for the exact SHA is green before deploying; + the deploy scripts pull published images and never build them. +- **`helm dependency build`, never `dependency update`, on the deploy path.** The engine resolves + subcharts from `Chart.lock` for reproducibility. `dependency update` re-resolves and can drift. + (Regenerating the lock/archives after a chart stamp is the one place `dependency update` is + correct — review the diff.) +- **A green `helm template`/CI render does not prove a live `helm upgrade` works.** Stateful + services need their PVC semantics, reconcile-retry, and Secret-change pod-roll trigger checked + before deploying — see the live-upgrade checklist in the deploy ledger. +- **Never deploy with floating tags.** Public releases require `sha-*` build tags or digests; the + qualified-release-image policy rejects `latest` and friends. Tag floating exists only for the + disposable local k3d smoke. +- **The tenant's openclaw version pin lives in `values.yaml`, not in code defaults.** +- **Watch the queue, not only the jobs.** The org has a fixed number of concurrent runners; a + workflow storm (or a hung job) can queue runs for 30+ minutes. If a run seems stuck before any + job started, it is queue starvation, not a slow job. +- **Develop red = every PR pays the k3d smoke.** See the skip proof above. Fixing develop first + is usually the fastest way to speed up everyone's PRs. +- **A cancelled develop push is normal.** The workflow's concurrency group replaces a queued push + run when a newer push arrives; only in-flight publishes are protected. + +## Release versions and migrations + +The full policy lives in [`docs/agents/versioning.md`](agents/versioning.md); this is the working +summary. + +- The root `package.json` version names the **repository train** (for example `0.9.2`). Each + train has an immutable manifest `releases/.json` recording every application's + `adaptedVersion`, chart version, and the database schema version that work together. +- **Only applications whose own files changed stamp to the root version.** An application that + changed only through a shared library, the root dependency set, or the lockfile keeps its + latest released version — published images are pinned by commit SHA, so the shared change + reaches it regardless. (Before this rule, every shared change failed CI until every manifest + entry was bumped by hand.) +- A directly changed application updates, together: its manifest entry, its `package.json` + version mirror, its `project.json` `metadata.release.adaptedVersion`, and its chart + `appVersion` where a chart exists. Version-only mirror edits are "stamp-only" and do not count + as changes themselves. +- A **changed chart** bumps its chart version to the root version and adds exactly one + `helm/migrations/-to-.json` transition; the umbrella's `Chart.lock` and packaged + archives are then regenerated and reviewed. +- A **database schema change** updates the clean target baseline and adds one adjacent, reviewed + SQL transition under `apps/opencrane/prisma/migrations/-to-/`, bound by digest. +- Adjacent minor trains (`0.8.x → 0.9.0`) are the only automatic transition. Patch, skipped-minor, + and major transitions require an approved `manualTransition` with a reason in the manifest. +- Once a version tag exists, that train's composition is frozen: any further change must advance + the train and create the next manifest. + +CI enforces all of this in the `test` job (`check:release-versioning`), diffed against the last +validated base, so a violation fails the PR — not the deploy. + +## Letting an AI agent manage your deployment + +The deploy path is scriptable end to end, which makes it a good fit for an agent-run loop (the +repository ships a `deploy` agent and a `/deploy-loop` skill that mutate clusters only through +the scripts above and triage every failure into a fix PR, an issue, or a design question). + +The one thing an agent must never see in plain text is credentials. The convention: + +- **`keys/` at the repository root is gitignored** (`/keys/*` in `.gitignore`). Put one secret + per file, named for what it is: + - `keys/initial-model-api-key` — the provider API key that seeds the first routable model. + The deploy reads it as an environment variable, never as a flag: + `OPENCRANE_INITIAL_MODEL_API_KEY="$(cat keys/initial-model-api-key)"` alongside + `--initial-model-provider `. + - `keys/zitadel-pat` — the Zitadel service-user PAT for organisation management once the + mode-scoped credential lands (tracked in the silo org-role issues); standalone silos get a + full-org credential, fleet-mode silos a claims-only one. +- The agent reads a key file straight into the environment of the one command that needs it and + never echoes it, logs it, or passes it as a command argument — the same custody rule the + scripts themselves follow (the API key is environment-only precisely to keep it out of command + history and Helm values). +- Everything else an agent needs is already non-secret: cluster context, base domain, tenant + name, image digests from the release manifest, and the deploy ledger for cross-run memory. + +With `keys/` populated, a fresh silo is one command the agent can compose, run, and verify — +and the post-deploy verification plus the run report tell it (and you) whether the cluster is +actually healthy. diff --git a/scripts/__tests__/affected-deployables.test.mjs b/scripts/__tests__/affected-deployables.test.mjs index 18a70d72e..ca7eb0bde 100644 --- a/scripts/__tests__/affected-deployables.test.mjs +++ b/scripts/__tests__/affected-deployables.test.mjs @@ -182,8 +182,9 @@ test("overlaps image preparation and imports one complete direct k3d batch", fun assert.match(smoke, /cert-manager jetstack\/cert-manager[\s\S]*?&[\s\S]*?CERT_MANAGER_INSTALL_PID=\$![\s\S]*?cnpg cnpg\/cloudnative-pg/u); assert.match(smoke, /if ! wait "\$CERT_MANAGER_INSTALL_PID"/u); assert.match(smoke, /docker buildx build --load/u); - assert.match(smoke, /--cache-from "type=gha,scope=\$\{project\},timeout=2m"/u); - assert.doesNotMatch(smoke, /--cache-to/u); + assert.match(smoke, /--cache-from "type=registry,ref=\$\{SMOKE_BUILD_CACHE\}:\$\{project\}"/u); + assert.match(smoke, /--cache-from "type=registry,ref=\$\{SMOKE_BUILD_CACHE_UNTRUSTED\}:\$\{project\}"/u); + assert.match(smoke, /--cache-to "type=registry,ref=\$\{SMOKE_BUILD_CACHE_EXPORT\}:\$\{project\},mode=max"/u); const imports = smoke.match(/k3d image import/g) ?? []; assert.equal(imports.length, 1); assert.match(smoke, /k3d image import "\$\{SMOKE_IMAGES\[@\]\}" --cluster "\$CLUSTER_NAME" --mode direct/u); @@ -212,7 +213,7 @@ test("keeps heavyweight remote qualification ahead of image publication", functi assert.match(workflow, /run: \.\/apps\/_infra\/deploy-k8s\/platform\/tests\/develop-smoke\.sh/u); assert.match(workflow, /inputs\.heavy_qualification == 'k3d'/u); assert.match(workflow, /inputs\.heavy_qualification == 'all'/u); - assert.match(workflow, /needs: \[prepare, test, develop_smoke, image_smoke\]/u); + assert.match(workflow, /needs: \[prepare, test, storybook_visual, develop_smoke, image_smoke\]/u); assert.match(developSmokeJob[0], /needs: prepare/u); assert.match(developSmokeJob[0], /needs\.prepare\.outputs\.develop_smoke_can_skip != 'true'/u); assert.match(workflow, /continue-on-error: true[\s\S]*?run: node scripts\/develop-smoke-baseline\.mjs/u); @@ -230,13 +231,17 @@ test("keeps heavyweight remote qualification ahead of image publication", functi assert.match(developSmokeJob[0], /SMOKE_AFFECTED_PROJECTS: \$\{\{ needs\.prepare\.outputs\.develop_smoke_projects \}\}/u); assert.match(developSmokeJob[0], /SMOKE_BASE_SHA: \$\{\{ needs\.prepare\.outputs\.nx_base \}\}/u); assert.match(developSmokeJob[0], /SMOKE_STORAGE_MODE: \$\{\{ needs\.prepare\.outputs\.develop_smoke_storage_mode \}\}/u); - assert.match(developSmokeJob[0], /uses: crazy-max\/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487/u); + assert.match(developSmokeJob[0], /SMOKE_BUILD_CACHE: \$\{\{ env\.REGISTRY \}\}\/\$\{\{ github\.repository_owner \}\}\/\$\{\{ env\.BUILD_CACHE_IMAGE \}\}/u); + assert.match(developSmokeJob[0], /SMOKE_BUILD_CACHE_EXPORT: /u); assert.match(imageSmokeJob[0], /matrix: \$\{\{ fromJSON\(needs\.prepare\.outputs\.image_smokes\) \}\}/u); assert.match( imageSmokeJob[0], /IMAGE_SMOKE_PROJECT: \$\{\{ matrix\.project \}\}[\s\S]*?npx nx run "\$IMAGE_SMOKE_PROJECT:image-smoke"/u, ); - assert.match(workflow, /cache-from: type=gha,scope=\$\{\{ matrix\.project \}\}/u); + assert.match( + workflow, + /type=registry,ref=\$\{\{ env\.REGISTRY \}\}\/\$\{\{ github\.repository_owner \}\}\/\$\{\{ env\.BUILD_CACHE_IMAGE \}\}:\$\{\{ matrix\.project \}\}/u, + ); assert.match(workflow, /type=raw,value=sha-\$\{\{ github\.sha \}\}/u); assert.match(publishSmokeImagesJob[0], /github\.ref == 'refs\/heads\/develop'/u); assert.match(publishSmokeImagesJob[0], /matrix: \$\{\{ fromJSON\(needs\.prepare\.outputs\.develop_smoke_images\) \}\}/u); From e3b94a9a8768b81f43e3ee29d942e92a5b6a5387 Mon Sep 17 00:00:00 2001 From: Jente Rosseel Date: Wed, 19 Aug 2026 14:59:43 +0300 Subject: [PATCH 6/9] =?UTF-8?q?=F0=9F=93=9D=20publish=20the=20contributing?= =?UTF-8?q?=20section:=20CI=20pipeline,=20deploys,=20versions,=20AI-manage?= =?UTF-8?q?d=20deployment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five pages under website/contributing ground the new docs/ci-and-deploy.md reference for site readers, wired into the nav and sidebar; the website stamps to 0.9.2 for its direct change. --- releases/0.9.2.json | 4 +- website/.vitepress/config.ts | 12 ++ website/contributing/ai-managed-deployment.md | 57 ++++++++ website/contributing/ci-pipeline.md | 138 ++++++++++++++++++ website/contributing/deploying.md | 97 ++++++++++++ website/contributing/overview.md | 61 ++++++++ .../contributing/versions-and-migrations.md | 75 ++++++++++ website/guide/deploy-cluster.md | 4 + website/operators/hosting.md | 5 +- website/package.json | 2 +- website/project.json | 2 +- 11 files changed, 451 insertions(+), 6 deletions(-) create mode 100644 website/contributing/ai-managed-deployment.md create mode 100644 website/contributing/ci-pipeline.md create mode 100644 website/contributing/deploying.md create mode 100644 website/contributing/overview.md create mode 100644 website/contributing/versions-and-migrations.md diff --git a/releases/0.9.2.json b/releases/0.9.2.json index 495c266f4..630393b82 100644 --- a/releases/0.9.2.json +++ b/releases/0.9.2.json @@ -15,8 +15,8 @@ "projects": { "@opencrane/website": { "root": "website", - "adaptedVersion": "0.9.1", - "packageVersion": "0.9.1" + "adaptedVersion": "0.9.2", + "packageVersion": "0.9.2" }, "agent-controller": { "root": "apps/agent-controller", diff --git a/website/.vitepress/config.ts b/website/.vitepress/config.ts index 0e3a4d17c..325d90eff 100644 --- a/website/.vitepress/config.ts +++ b/website/.vitepress/config.ts @@ -51,6 +51,7 @@ export default defineConfig({ { text: 'API overview', link: '/reference/api-overview' }, ], }, + { text: 'Contributing', link: '/contributing/overview' }, { text: 'GitHub', link: REPO }, ], @@ -135,6 +136,17 @@ export default defineConfig({ { text: 'Running multiple instances', link: '/advanced/multi-instance' }, ], }, + { + text: 'Contributing', + collapsed: true, + items: [ + { text: 'Overview', link: '/contributing/overview' }, + { text: 'The CI pipeline', link: '/contributing/ci-pipeline' }, + { text: 'Deploying', link: '/contributing/deploying' }, + { text: 'Versions and migrations', link: '/contributing/versions-and-migrations' }, + { text: 'Letting an AI agent manage your deployment', link: '/contributing/ai-managed-deployment' }, + ], + }, ], socialLinks: [{ icon: 'github', link: REPO }], diff --git a/website/contributing/ai-managed-deployment.md b/website/contributing/ai-managed-deployment.md new file mode 100644 index 000000000..df6be6caa --- /dev/null +++ b/website/contributing/ai-managed-deployment.md @@ -0,0 +1,57 @@ +# Letting an AI agent manage your deployment + +The deploy path described in [Deploying](/contributing/deploying) is scriptable end to end, +which makes it a good fit for an agent-run loop. This page covers the credential convention that +keeps that safe, and the agent that uses it. + +> See also: [Deploying](/contributing/deploying) (the scripts the agent runs), [The CI pipeline]( +> /contributing/ci-pipeline) (what must be green before an agent deploys), and +> [Versions and migrations](/contributing/versions-and-migrations) (what the agent must resolve +> before mutating a cluster). + +The repository ships a `deploy` agent and a `/deploy-loop` skill that mutate clusters **only** +through the scripts covered in [Deploying](/contributing/deploying), and triage every failure +into a fix pull request, an issue, or a design question. The one thing an agent must never see +in plain text is a credential. + +## The `keys/` convention + +`keys/` at the repository root is gitignored (`/keys/*` in `.gitignore`). Put one secret per +file, named for what it is: + +| File | Contents | Consumed as | +| --- | --- | --- | +| `keys/initial-model-api-key` | The provider API key that seeds the first routable model | `OPENCRANE_INITIAL_MODEL_API_KEY="$(cat keys/initial-model-api-key)"`, alongside `--initial-model-provider ` | +| `keys/zitadel-pat` | The Zitadel service-user PAT for organisation management, once the mode-scoped credential lands 🔶 | Standalone silos get a full-org credential; fleet-mode silos get a claims-only one | + +::: warning +`keys/zitadel-pat` describes an upcoming credential shape, not something the deploy scripts +consume today. Treat it as an intended destination for the mode-scoped Zitadel credential, not a +currently wired flag. +::: + +## The custody rule + +The agent reads a key file straight into the environment of the one command that needs it, and +never echoes it, logs it, or passes it as a command argument — the same rule the deploy scripts +themselves follow. `OPENCRANE_INITIAL_MODEL_API_KEY` is environment-only precisely to keep the +key out of command history and Helm values. + +Everything else an agent needs is already non-secret: cluster context, base domain, tenant name, +image digests from the release manifest, and the deploy ledger for cross-run memory. + +## The deploy agent and `/deploy-loop` + +- The **`deploy` agent** mutates a cluster only through the deploy scripts, reads whatever it + needs for diagnosis (read-only `kubectl`, `helm status`, read-only SQL), and returns a + structured run report. +- The **`/deploy-loop` skill** orchestrates a full run: it resolves the exact release manifest + and database transition first, spawns one `deploy` agent, then triages every finding in the + report into a chart/script fix, a codebase issue, a data issue, or an infra/design question. + +With `keys/` populated, a fresh silo is one command an agent can compose, run and verify — and +the post-deploy verification plus the run report tell it, and you, whether the cluster is +actually healthy. + +Source: [`.claude/agents/deploy.md`](https://github.com/elewa-git/opencrane/blob/main/.claude/agents/deploy.md) +and [`.claude/commands/deploy-loop.md`](https://github.com/elewa-git/opencrane/blob/main/.claude/commands/deploy-loop.md). diff --git a/website/contributing/ci-pipeline.md b/website/contributing/ci-pipeline.md new file mode 100644 index 000000000..347de7ff6 --- /dev/null +++ b/website/contributing/ci-pipeline.md @@ -0,0 +1,138 @@ +# The CI pipeline + +Every pull request and every push to `develop` or `main` runs through **three GitHub Actions +workflows**. This page covers what each job gates and the caching that keeps the pipeline fast +enough to stay in the loop. + +> See also: [Contributing overview](/contributing/overview) (where this fits in the PR-to-cluster +> journey), [Deploying](/contributing/deploying) (what consumes the images this pipeline +> publishes), and [Versions and migrations](/contributing/versions-and-migrations) (the +> `check:release-versioning` gate enforced inside the `test` job). + +## The three workflows + +| Workflow | File | Purpose | Typical duration | +| --- | --- | --- | --- | +| Validate and publish affected deployables | [`docker.yml`](https://github.com/elewa-git/opencrane/blob/main/.github/workflows/docker.yml) | The main pipeline: build, test, smoke and publish | minutes (see below) | +| Enforce pull-request stack integrity | [`pr-stack-integrity.yml`](https://github.com/elewa-git/opencrane/blob/main/.github/workflows/pr-stack-integrity.yml) | Stacked-PR bookkeeping | under a minute | +| CodeQL | GitHub's default code-scanning setup | Static analysis | a few minutes | + +`docker.yml` is where the time goes, so the rest of this page is about its jobs. + +## The pipeline fan-out + +```text +pull request / push to develop, main + │ + ▼ + prepare + computes the affected graph, the deployable matrix, the guard + comparison base, and whether the k3d smoke can be skipped + │ + ├──→ test build, test, lint, every policy guard + ├──→ storybook_visual component contracts, cached Chromium + ├──→ develop_smoke k3d silo smoke — the long pole + └──→ image_smoke per-image boot checks + │ + ▼ (all four must pass) + build-and-push + publishes sha- images on push events; + on pull requests it builds without pushing, as a proof + │ + ▼ + publish-develop-smoke-images + develop pushes only: completes the immutable image + set for the commit, reusing untouched base images +``` + +## What each job gates + +| Job | Purpose | Typical duration | +| --- | --- | --- | +| `prepare` | Computes the Nx affected graph, the deployable matrix, the guard comparison base, and whether the k3d smoke can be skipped. | 1–2 min | +| `test` | Builds, tests and lints affected projects, and runs every policy guard: workload ownership, agent-domain boundary, mechanical style, module growth, release versioning, database migration contracts, Prisma boundaries, config-docs coverage, dependency boundaries. | 3–12 min | +| `storybook_visual` | Storybook build/behaviour/visual contracts for affected frontend projects, on cached Chromium. Runs beside `test`, not after it. | seconds when nothing affected; ~5 min otherwise | +| `develop_smoke` | Boots a disposable k3d cluster, deploys the full current silo through the real deploy scripts, and proves database isolation, TLS ingress and workload health. | 6–15 min | +| `image_smoke` | Boots individual images that declare an `image-smoke` target and checks they come up. | 1–2 min per image | +| `build-and-push` | Builds every affected deployable image and publishes `sha-` tags on push events. On pull requests it builds without pushing, as a proof. | 1–5 min warm | +| `publish-develop-smoke-images` | On develop pushes, completes the immutable image set for the commit: reuses the exact validated base image where nothing changed, copies tags forward, builds only what is missing. | seconds per image | + +A deploy to a live cluster consumes the published `sha-` images. CI must be green for the +exact SHA before any deploy — the deploy scripts pull images, never build them (see +[Deploying](/contributing/deploying)). + +## Caching layers + +Every job runs on a fresh runner, so anything not cached is paid on every run. + +| Cache | Backend | Key | Used by | +| --- | --- | --- | --- | +| npm download cache | `actions/setup-node` | lockfile hash | all jobs | +| `node_modules` | `actions/cache` | lockfile hash | all jobs (skips `npm ci` entirely on a hit) | +| Nx computation cache | `actions/cache` (`.nx/cache`) | lockfile hash + commit, with prefix restore | `test`, `storybook_visual` | +| Playwright Chromium | `actions/cache` (`~/.cache/ms-playwright`) | lockfile hash | `storybook_visual` | +| Docker image layers | registry (`ghcr.io//opencrane-buildcache:`) | buildx layer graph | `develop_smoke`, `build-and-push`, `publish-develop-smoke-images` | +| npm inside Dockerfiles | BuildKit cache mount (`/root/.npm`) | shared between build and runtime stages within one build | all Node images | + +### Why image layers cache in the registry, not the Actions cache + +The repository's 10GB Actions cache quota was permanently over budget — single buildkit blobs +reach ~700MB — so layer caches were evicted almost immediately and every image built cold. +Registry-backed caches (`type=registry`) have no such quota, and moving them out of the Actions +cache also stops the node/Nx caches from being evicted alongside them. + +### The layer cache has a trust boundary + +Two cache repositories exist, and a layer produced by unreviewed code never becomes part of a +published image: + +```text +same-repository pull request build + reads ← opencrane-buildcache (trusted: written only by develop/main pushes) + writes → opencrane-buildcache-pr (pull-request cache, read back by later PR builds) + +fork pull request build + reads ← opencrane-buildcache (trusted; fork tokens are read-only) + writes → nothing + +develop / main push + reads ← opencrane-buildcache + writes → opencrane-buildcache (becomes the new trusted layer set) +``` + +`opencrane-buildcache` is the only cache a publishable build reads. Same-repository pull +requests write to the separate `opencrane-buildcache-pr` cache; fork pull requests read the +trusted cache and write nothing at all. + +::: info +Chromium binaries restore from the Actions cache; the apt-driven `--with-deps` install runs only +on a cold cache and carries a ten-minute step timeout, because a hung apt once held an untimed +job — and its runner — for hours. Every job in the pipeline has a `timeout-minutes`: a hung job +does not only lose its own time, it occupies a concurrent-runner slot and starves every queued +run behind it. +::: + +## The k3d smoke and its skip proof + +`develop_smoke` is the pipeline's long pole and its most valuable gate: it exercises the real +deploy path end to end on every develop push. Two mechanisms keep its cost down: + +1. **Image reuse by digest.** Projects the affected graph did not select are pulled from the + validated base commit's published images instead of being rebuilt + ([`develop-smoke.sh`](https://github.com/elewa-git/opencrane/blob/main/apps/_infra/deploy-k8s/platform/tests/develop-smoke.sh)`:_pull_baseline_image`). +2. **The skip proof.** A pull request skips the k3d smoke entirely when its exact base SHA + already completed the same k3d job successfully + ([`scripts/develop-smoke-baseline.mjs`](https://github.com/elewa-git/opencrane/blob/main/scripts/develop-smoke-baseline.mjs)). + Any ambiguity — API failure, missing proof, affected containers — deliberately runs the + smoke instead of skipping it. + +::: tip +The skip proof is why **keeping develop green is a speed feature**: a red develop push means no +validated base exists, so every subsequent pull request pays the full smoke. A failure streak on +develop taxes every open PR in the repository — fixing develop first is usually the fastest way +to speed up everyone else's work. +::: + +The smoke's storage tier also varies: touching `k8s-deploy.sh`, the smoke script, the workflow, +or `apps/postgres/` selects the `full` tier, which additionally exercises CSI volume expansion +(several extra minutes). Other changes run the `fast` tier. diff --git a/website/contributing/deploying.md b/website/contributing/deploying.md new file mode 100644 index 000000000..ab4d163e8 --- /dev/null +++ b/website/contributing/deploying.md @@ -0,0 +1,97 @@ +# Deploying + +All cluster mutation goes through **app-owned scripts** — never bare `helm upgrade` or `kubectl +apply` against a live cluster. This page covers the deploy chain, its prerequisites, and the +warnings that save hours of debugging. + +> See also: [The CI pipeline](/contributing/ci-pipeline) (the images this chain consumes), +> [Hosting and deployment](/operators/hosting) (the operator-facing install path), and +> [Runbook](/operators/runbook) (diagnosing a cluster once it is live). + +## The script-only rule + +Every deploy — from a contributor's k3d smoke to a production silo — runs through the same +entrypoints. There is no supported path that touches a live cluster by hand. + +```text +apps/_infra/deploy-k8s/deploy.sh + silo profile: flags, presets + │ + ▼ +platform/k8s-deploy.sh + the install engine + │ + ├──→ current-chart-sources.sh + │ helm dependency build from Chart.lock + │ + ├──→ database-migration-orchestrator.sh + │ CNPG cluster, databases, migration + privileges Jobs + │ + ├──→ umbrella helm upgrade + │ all app subcharts + │ │ + │ ▼ + │ database-release-finalization.sh + │ credential-checksum roll, rollout waits, cert wait + │ + └──→ post-deploy-verify.sh + live health verification +``` + +- `deploy.sh` installs one per-ClusterTenant silo: operator, channel proxy, LiteLLM, Cognee, + opencrane-ui, per-CT networking, and one app-owned PostgreSQL server with isolated logical + databases. Required flags: `--base-domain`, `--cluster-tenant`, `--acme-email`, + `--first-user-email`; fresh installs also need `--opencrane-ui-digest` and `--cognee-digest` + (immutable digests, never tags). +- Cluster-wide prerequisites (ingress-nginx, cert-manager, CloudNativePG) are installed once per + cluster by `bootstrap-prerequisites.sh` and are never part of a silo release. +- The PostgreSQL transition is resolved and schema-validated *before* the cluster is touched; + migration and privileges run as bounded Helm hook Jobs, with an automatic rollback path that + restores the exact fenced Helm revision on failure. +- After the umbrella upgrade, the engine stamps a checksum of the published database connection + Secrets onto the consumer Deployments (`opencrane-server`, `litellm`, `mcp-gateway`). An + unchanged checksum is a no-op; a changed one triggers exactly one rollout. This replaced an + unconditional `rollout restart` that double-started the heaviest workloads on every deploy. + +## Bootstrap prerequisites + +Before any silo installs, the cluster needs a default StorageClass, a `NetworkPolicy`-enforcing +CNI, and — via `bootstrap-prerequisites.sh` — ingress-nginx, cert-manager and CloudNativePG. These +are cluster-wide and installed once; the silo chart assumes them and never re-installs them. + +## Warnings — read before deploying + +::: warning CI green first +Confirm the `docker.yml` run for the exact SHA is green before deploying. The deploy scripts pull +published images and never build them — see [The CI pipeline](/contributing/ci-pipeline). +::: + +::: warning `helm dependency build`, never `dependency update`, on the deploy path +The engine resolves subcharts from `Chart.lock` for reproducibility. `dependency update` +re-resolves and can drift. (Regenerating the lock and archives after a chart version stamp is the +one place `dependency update` is correct — review the diff. See +[Versions and migrations](/contributing/versions-and-migrations).) +::: + +::: warning A green render does not prove a live upgrade works +A passing `helm template` or CI render does not prove a live `helm upgrade` works. Stateful +services need their PVC semantics, reconcile-retry, and Secret-change pod-roll trigger checked +before deploying — see the live-upgrade checklist referenced from the deploy ledger. +::: + +::: warning Never deploy with floating tags +Public releases require `sha-*` build tags or digests; the qualified-release-image policy rejects +`latest` and similar tags. Tag floating exists only for the disposable local k3d smoke. +::: + +- **The tenant's openclaw version pin lives in `values.yaml`, not in code defaults.** +- **Watch the queue, not only the jobs.** The organisation has a fixed number of concurrent + runners; a workflow storm (or a hung job) can queue runs for 30+ minutes. If a run seems stuck + before any job has started, that is queue starvation, not a slow job. +- **Develop red means every PR pays the k3d smoke.** See + [the skip proof](/contributing/ci-pipeline#the-k3d-smoke-and-its-skip-proof). Fixing develop + first is usually the fastest way to speed up everyone's pull requests. +- **A cancelled develop push is normal.** The workflow's concurrency group replaces a queued push + run when a newer push arrives; only in-flight publishes are protected. + +Source: [`apps/_infra/deploy-k8s`](https://github.com/elewa-git/opencrane/blob/main/apps/_infra/deploy-k8s/README.md). diff --git a/website/contributing/overview.md b/website/contributing/overview.md new file mode 100644 index 000000000..6bb20a997 --- /dev/null +++ b/website/contributing/overview.md @@ -0,0 +1,61 @@ +# Contributing to OpenCrane + +This section is for people changing the **OpenCrane codebase itself**: what happens between +opening a pull request and a change running on a live cluster, and how to work with that +pipeline instead of against it. + +> See also: [Cluster deployment](/guide/deploy-cluster) (the operator-facing install path this +> pipeline publishes images for) and [Hosting and deployment](/operators/hosting) (what a +> release owns once it is live). + +## The journey from pull request to running cluster + +A change passes through the same three stages whether it is a one-line fix or a new subsystem: + +```text +pull request opened + │ + ▼ + CI pipeline runs three workflows validate the change; + (see: The CI pipeline) the longest is the k3d silo smoke + │ + ▼ + merge to develop or main + │ + ▼ + images published sha- tags, immutable, never floating + │ + ▼ + deploy scripts run apps/_infra/deploy-k8s/deploy.sh → k8s-deploy.sh + (see: Deploying) pulls the exact published images, never builds them + │ + ▼ + live cluster, verified post-deploy-verify.sh checks real health +``` + +Two threads run underneath every step of that journey: + +- **Versioning.** Every directly changed application, chart and database schema stamps forward + together, and CI enforces the whole scheme before anything reaches the deploy path — see + [Versions and migrations](/contributing/versions-and-migrations). +- **Automation.** The same deploy scripts an operator would run by hand are scriptable enough + for an agent to run, verify and triage — see + [Letting an AI agent manage your deployment](/contributing/ai-managed-deployment). + +## In this section + +| Page | Covers | +| --- | --- | +| [The CI pipeline](/contributing/ci-pipeline) | The three workflows, what each `docker.yml` job gates, and the caching layers that keep it fast | +| [Deploying](/contributing/deploying) | The script-only rule, the deploy chain, bootstrap prerequisites, and the warnings that save hours | +| [Versions and migrations](/contributing/versions-and-migrations) | The repository train, the stamp rule, chart and database migrations, and how CI enforces them | +| [Letting an AI agent manage your deployment](/contributing/ai-managed-deployment) | The gitignored `keys/` convention, credential custody, and the deploy agent + `/deploy-loop` skill | + +::: tip +Keeping the `develop` branch green is the single highest-leverage thing a contributor can do: +a red `develop` means every open pull request pays the full k3d smoke instead of skipping it. +See [the skip proof](/contributing/ci-pipeline#the-k3d-smoke-and-its-skip-proof). +::: + +Source: [`docs/ci-and-deploy.md`](https://github.com/elewa-git/opencrane/blob/main/docs/ci-and-deploy.md) +is the deeper repository-side reference this section publishes from. diff --git a/website/contributing/versions-and-migrations.md b/website/contributing/versions-and-migrations.md new file mode 100644 index 000000000..80e5af0cc --- /dev/null +++ b/website/contributing/versions-and-migrations.md @@ -0,0 +1,75 @@ +# Versions and migrations + +OpenCrane ships one **repository train** made of many independently versioned applications, +Helm charts and a shared database schema. This page covers how a change stamps its version, +how chart and database migrations are recorded, and how CI enforces all of it. + +> See also: [The CI pipeline](/contributing/ci-pipeline) (where `check:release-versioning` runs), +> [Deploying](/contributing/deploying) (why `helm dependency build`, never `dependency update`, +> matters here), and the full policy in +> [`docs/agents/versioning.md`](https://github.com/elewa-git/opencrane/blob/main/docs/agents/versioning.md). + +## The repository train and the release manifest + +The root `package.json` version names the current train (for example `0.9.2`). Each train has an +immutable manifest, [`releases/.json`](https://github.com/elewa-git/opencrane/blob/main/releases), +recording every application's `adaptedVersion`, chart version and the database schema version +that work together. Once a version tag exists, that train's composition is frozen: any further +change must advance the train and create the next manifest. + +## The stamp rule + +Only applications whose own files changed stamp to the root version: + +```text +did this application's own project-root files change? + │ + yes ─┴─ no + │ │ + ▼ ▼ + stamp to keep its latest + the root released version + version │ + │ (a shared library, the root + ▼ dependency set, or the lockfile +update, together: moving underneath it does + · manifest entry not trigger a stamp — the + · package.json published image is pinned + version mirror by commit SHA, so the + · project.json shared change reaches the + adaptedVersion app regardless) + · chart appVersion + (if a chart exists) +``` + +Before this rule, every shared change failed CI until every manifest entry was bumped by hand. +Version-only mirror edits are "stamp-only" and do not count as changes themselves. + +## Chart migrations + +A changed chart bumps its chart version to the root version and adds exactly one +`helm/migrations/-to-.json` transition. The umbrella's `Chart.lock` and packaged +archives are then regenerated and reviewed (with `helm dependency build`, never +`dependency update` — see [Deploying](/contributing/deploying)). A newly introduced chart has no +predecessor and therefore no transition. + +## Database migrations + +A database schema change updates the clean target baseline and adds one adjacent, reviewed SQL +transition under +[`apps/opencrane/prisma/migrations/-to-/`](https://github.com/elewa-git/opencrane/blob/main/apps/opencrane/prisma/migrations), +bound by digest. + +- **Adjacent minor trains** (`0.8.x → 0.9.0`) are the only automatic transition. +- **Patch, skipped-minor, and major transitions** require an approved `manualTransition` with a + reason recorded in the manifest — tooling never guesses the upgrade path. + +## CI enforces the whole scheme + +::: tip +`check:release-versioning` runs inside the `test` job of [`docker.yml`](/contributing/ci-pipeline), +diffed against the last validated base. A violation fails the pull request — not the deploy. +::: + +Source: [`docs/agents/versioning.md`](https://github.com/elewa-git/opencrane/blob/main/docs/agents/versioning.md) +and [`scripts/release-versioning-check.mjs`](https://github.com/elewa-git/opencrane/blob/main/scripts/release-versioning-check.mjs). diff --git a/website/guide/deploy-cluster.md b/website/guide/deploy-cluster.md index 42876766f..42c6b18ee 100644 --- a/website/guide/deploy-cluster.md +++ b/website/guide/deploy-cluster.md @@ -55,3 +55,7 @@ Keep provider-specific identity and storage configuration outside the runtime au ## Next → [Set up your domain](/guide/dns) → [Set up your personal assistant](/guide/persona) + +Changing OpenCrane itself rather than installing it? See +[Contributing → Deploying](/contributing/deploying) for the CI-to-cluster pipeline this script +sits behind. diff --git a/website/operators/hosting.md b/website/operators/hosting.md index 7324b77db..3f052862c 100644 --- a/website/operators/hosting.md +++ b/website/operators/hosting.md @@ -5,8 +5,9 @@ umbrella chart composes the trusted services and separate, restricted Job namesp > See also: [Deployment configuration](/operators/deployment-configuration) (public Helm inputs), > [Organisation boundary](/operators/organisation-boundary) (what one silo serves), -> [Networking and isolation](/operators/networking) (allowed traffic), and -> [Runbook](/operators/runbook) (health and recovery). +> [Networking and isolation](/operators/networking) (allowed traffic), +> [Runbook](/operators/runbook) (health and recovery), and +> [Deploying](/contributing/deploying) (the script chain and CI gates behind a release). ## Deployment shape diff --git a/website/package.json b/website/package.json index 4d027aa2b..78c4bd674 100644 --- a/website/package.json +++ b/website/package.json @@ -1,6 +1,6 @@ { "name": "@opencrane/website", - "version": "0.9.1", + "version": "0.9.2", "private": true, "description": "OpenCrane documentation website (VitePress)", "license": "AGPL-3.0-or-later", diff --git a/website/project.json b/website/project.json index 482c88de0..763a9cfea 100644 --- a/website/project.json +++ b/website/project.json @@ -2,5 +2,5 @@ "name": "@opencrane/website", "$schema": "../node_modules/nx/schemas/project-schema.json", "projectType": "application", - "metadata": { "release": { "adaptedVersion": "0.9.1" } } + "metadata": { "release": { "adaptedVersion": "0.9.2" } } } From 795046ea2cdf4dfba2a272318f1f40915b4a0fc8 Mon Sep 17 00:00:00 2001 From: Jente Rosseel Date: Wed, 19 Aug 2026 15:07:59 +0300 Subject: [PATCH 7/9] =?UTF-8?q?=E2=9A=A1=20split=20the=20database=20proofs?= =?UTF-8?q?=20and=20API=20contract=20check=20into=20parallel=20jobs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PostgreSQL-bound work (migration convergence, generated client, target baseline, SQL authority suites) and the API-contract server rebuild ran serially inside the test job. Each now runs as its own parallel job gating publication, taking minutes off the test job's critical path. --- .github/workflows/docker.yml | 124 +++++++++++++++--- docs/ci-and-deploy.md | 12 +- .../__tests__/affected-deployables.test.mjs | 2 +- website/contributing/ci-pipeline.md | 10 +- 4 files changed, 123 insertions(+), 25 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b20b2ab7d..0a594d423 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -254,12 +254,6 @@ jobs: - name: Prove the release-versioning classifier run: npm run test:release-versioning - - name: Enforce version-to-version database migration contracts - run: npm run check:database-migrations - - - name: Prove previous-to-current database convergence and rollback - run: npm run test:database-migrations - - name: Prove the PR-stack integrity classifier run: npm run test:pr-stack-integrity @@ -275,8 +269,78 @@ jobs: - name: Prove the Prisma-boundary classifier run: npm run test:prisma-boundaries - # The topology gate must inspect the checked-in app owners, before any - # generator can introduce transient files beneath an app root. + - name: Restore the Nx computation cache + uses: actions/cache@v4 + with: + path: .nx/cache + key: nx-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-${{ github.sha }} + restore-keys: | + nx-${{ runner.os }}-${{ hashFiles('package-lock.json') }}- + + - name: Build, test, and lint affected projects + run: npx nx affected -t build test lint + + - name: Enforce monorepo dependency boundaries + run: npm run lint:boundaries + + # The PostgreSQL-bound proofs run beside the main test job instead of serially inside it: + # database generation, the target baseline, the SQL authority suites, and the migration + # convergence proofs together took minutes off the test job's critical path. + database: + name: Database authority and migration proofs + runs-on: ubuntu-latest + timeout-minutes: 30 + needs: prepare + services: + postgres: + image: postgres:17 + env: + POSTGRES_DB: opencrane + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres -d opencrane" + --health-interval 5s + --health-timeout 5s + --health-retries 12 + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/opencrane + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + + - name: Restore node_modules + id: node-modules + uses: actions/cache@v4 + with: + path: node_modules + key: node-modules-${{ runner.os }}-node24-${{ hashFiles('package-lock.json') }} + + - name: Install dependencies + if: steps.node-modules.outputs.cache-hit != 'true' + run: npm ci + + # The runner image ships the PostgreSQL client; install it only if that ever changes. + - name: Install the PostgreSQL client + run: | + if ! command -v psql >/dev/null; then + sudo apt-get update && sudo apt-get install --yes --no-install-recommends postgresql-client + fi + + - name: Enforce version-to-version database migration contracts + run: npm run check:database-migrations + + - name: Prove previous-to-current database convergence and rollback + run: npm run test:database-migrations + - name: Generate the database client run: npm run db:generate -w @opencrane/server @@ -291,6 +355,33 @@ jobs: - name: Run every PostgreSQL authority suite run: npx nx run-many -t test:sql --parallel=1 + # The API contract check builds the full server, so when the contract changed it added + # minutes to the test job. It only needs the prepare outputs and runs beside it instead. + api_contract: + name: API reference and generated client + runs-on: ubuntu-latest + timeout-minutes: 20 + needs: prepare + if: needs.prepare.outputs.api_contract_changed == 'true' + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + + - name: Restore node_modules + id: node-modules + uses: actions/cache@v4 + with: + path: node_modules + key: node-modules-${{ runner.os }}-node24-${{ hashFiles('package-lock.json') }} + + - name: Install dependencies + if: steps.node-modules.outputs.cache-hit != 'true' + run: npm ci + - name: Restore the Nx computation cache uses: actions/cache@v4 with: @@ -299,14 +390,7 @@ jobs: restore-keys: | nx-${{ runner.os }}-${{ hashFiles('package-lock.json') }}- - - name: Build, test, and lint affected projects - run: npx nx affected -t build test lint - - - name: Enforce monorepo dependency boundaries - run: npm run lint:boundaries - - - name: Verify API reference and generated client when affected - if: needs.prepare.outputs.api_contract_changed == 'true' + - name: Verify API reference and generated client run: | npx nx run opencrane:build npm run sync-openapi -w @opencrane/website @@ -518,12 +602,14 @@ jobs: name: Build and publish affected images runs-on: ubuntu-latest timeout-minutes: 30 - needs: [prepare, test, storybook_visual, develop_smoke, image_smoke] + needs: [prepare, test, database, api_contract, storybook_visual, develop_smoke, image_smoke] if: >- ${{ always() && needs.prepare.outputs.has_deployables == 'true' && needs.test.result == 'success' && + needs.database.result == 'success' && + (needs.api_contract.result == 'success' || needs.api_contract.result == 'skipped') && needs.storybook_visual.result == 'success' && (needs.develop_smoke.result == 'success' || needs.develop_smoke.result == 'skipped') && (needs.image_smoke.result == 'success' || needs.image_smoke.result == 'skipped') @@ -578,13 +664,15 @@ jobs: name: Publish develop smoke image (${{ matrix.project }}) runs-on: ubuntu-latest timeout-minutes: 20 - needs: [prepare, test, storybook_visual, develop_smoke, image_smoke, build-and-push] + needs: [prepare, test, database, api_contract, storybook_visual, develop_smoke, image_smoke, build-and-push] if: >- ${{ always() && github.event_name == 'push' && github.ref == 'refs/heads/develop' && needs.test.result == 'success' && + needs.database.result == 'success' && + (needs.api_contract.result == 'success' || needs.api_contract.result == 'skipped') && needs.storybook_visual.result == 'success' && needs.develop_smoke.result == 'success' && (needs.image_smoke.result == 'success' || needs.image_smoke.result == 'skipped') && diff --git a/docs/ci-and-deploy.md b/docs/ci-and-deploy.md index 40bc0708b..4d63048cc 100644 --- a/docs/ci-and-deploy.md +++ b/docs/ci-and-deploy.md @@ -18,11 +18,15 @@ main pipeline is where the time goes: ```mermaid flowchart LR - P[prepare\ncalculate affected work\n~1-2 min] --> T[test\nbuild, test, lint,\npolicy guards\n~3-12 min] + P[prepare\ncalculate affected work\n~1-2 min] --> T[test\nbuild, test, lint,\npolicy guards\n~3-10 min] + P --> DB[database\nSQL authority suites,\nmigration proofs\n~2-4 min] + P --> A[api_contract\nOpenAPI + client sync\nonly when affected] P --> S[storybook_visual\ncomponent contracts\nskips fast when\nnothing affected] P --> K[develop_smoke\nk3d current-silo smoke\n~6-15 min] P --> I[image_smoke\nper-image boot checks\n~1-2 min each] T --> B[build-and-push\npublish affected images\npush events only] + DB --> B + A --> B S --> B K --> B I --> B @@ -35,7 +39,9 @@ What each job owns: | Job | Purpose | Typical duration | | --- | --- | --- | | `prepare` | Computes the Nx affected graph, the deployable matrix, the guard comparison base, and whether the k3d smoke can be skipped. | 1–2 min | -| `test` | Builds, tests, and lints affected projects, and runs every policy guard: workload ownership, agent-domain boundary, mechanical style, module growth, release versioning, database migration contracts, Prisma boundaries, config-docs coverage, dependency boundaries. | 3–12 min | +| `test` | Builds, tests, and lints affected projects, and runs every policy guard: workload ownership, agent-domain boundary, mechanical style, module growth, release versioning, Prisma boundaries, config-docs coverage, dependency boundaries. | 3–10 min | +| `database` | Everything PostgreSQL-bound, beside `test` instead of inside it: the migration contracts and convergence proofs, the generated client, the target baseline, and the SQL authority suites. | 2–4 min | +| `api_contract` | Rebuilds the server and proves the OpenAPI reference and generated client are in sync. Runs only when the API contract changed. | skipped, or ~3–5 min | | `storybook_visual` | Storybook build/behaviour/visual contracts for affected frontend projects, on cached Chromium. Runs beside `test`, not after it. | seconds when nothing affected; ~5 min otherwise | | `develop_smoke` | Boots a disposable k3d cluster, deploys the full current silo through the real deploy scripts, and proves database isolation, TLS ingress, and workload health. The long pole of the pipeline. | 6–15 min | | `image_smoke` | Boots individual images that declare an `image-smoke` target and checks they come up. | 1–2 min per image | @@ -54,7 +60,7 @@ these caches, from cheapest to most impactful: | --- | --- | --- | --- | | npm download cache | `actions/setup-node` | lockfile hash | all jobs | | `node_modules` | `actions/cache` | lockfile hash | all jobs (skips `npm ci` entirely on a hit) | -| Nx computation cache | `actions/cache` (`.nx/cache`) | lockfile hash + commit, with prefix restore | `test`, `storybook_visual` | +| Nx computation cache | `actions/cache` (`.nx/cache`) | lockfile hash + commit, with prefix restore | `test`, `api_contract`, `storybook_visual` | | Playwright Chromium | `actions/cache` (`~/.cache/ms-playwright`) | lockfile hash | `storybook_visual` | | Docker image layers | **registry** (`ghcr.io//opencrane-buildcache:`) | buildx layer graph | `develop_smoke`, `build-and-push`, `publish-develop-smoke-images` | | npm inside Dockerfiles | BuildKit cache mount (`/root/.npm`) | shared between build and runtime stages within one build | all Node images | diff --git a/scripts/__tests__/affected-deployables.test.mjs b/scripts/__tests__/affected-deployables.test.mjs index ca7eb0bde..82435db02 100644 --- a/scripts/__tests__/affected-deployables.test.mjs +++ b/scripts/__tests__/affected-deployables.test.mjs @@ -213,7 +213,7 @@ test("keeps heavyweight remote qualification ahead of image publication", functi assert.match(workflow, /run: \.\/apps\/_infra\/deploy-k8s\/platform\/tests\/develop-smoke\.sh/u); assert.match(workflow, /inputs\.heavy_qualification == 'k3d'/u); assert.match(workflow, /inputs\.heavy_qualification == 'all'/u); - assert.match(workflow, /needs: \[prepare, test, storybook_visual, develop_smoke, image_smoke\]/u); + assert.match(workflow, /needs: \[prepare, test, database, api_contract, storybook_visual, develop_smoke, image_smoke\]/u); assert.match(developSmokeJob[0], /needs: prepare/u); assert.match(developSmokeJob[0], /needs\.prepare\.outputs\.develop_smoke_can_skip != 'true'/u); assert.match(workflow, /continue-on-error: true[\s\S]*?run: node scripts\/develop-smoke-baseline\.mjs/u); diff --git a/website/contributing/ci-pipeline.md b/website/contributing/ci-pipeline.md index 347de7ff6..5cb1abdba 100644 --- a/website/contributing/ci-pipeline.md +++ b/website/contributing/ci-pipeline.md @@ -30,11 +30,13 @@ pull request / push to develop, main comparison base, and whether the k3d smoke can be skipped │ ├──→ test build, test, lint, every policy guard + ├──→ database SQL authority suites, migration proofs + ├──→ api_contract OpenAPI + generated client (when affected) ├──→ storybook_visual component contracts, cached Chromium ├──→ develop_smoke k3d silo smoke — the long pole └──→ image_smoke per-image boot checks │ - ▼ (all four must pass) + ▼ (all must pass) build-and-push publishes sha- images on push events; on pull requests it builds without pushing, as a proof @@ -50,7 +52,9 @@ pull request / push to develop, main | Job | Purpose | Typical duration | | --- | --- | --- | | `prepare` | Computes the Nx affected graph, the deployable matrix, the guard comparison base, and whether the k3d smoke can be skipped. | 1–2 min | -| `test` | Builds, tests and lints affected projects, and runs every policy guard: workload ownership, agent-domain boundary, mechanical style, module growth, release versioning, database migration contracts, Prisma boundaries, config-docs coverage, dependency boundaries. | 3–12 min | +| `test` | Builds, tests and lints affected projects, and runs every policy guard: workload ownership, agent-domain boundary, mechanical style, module growth, release versioning, Prisma boundaries, config-docs coverage, dependency boundaries. | 3–10 min | +| `database` | Everything PostgreSQL-bound, beside `test` instead of inside it: the migration contracts and convergence proofs, the generated client, the target baseline, and the SQL authority suites. | 2–4 min | +| `api_contract` | Rebuilds the server and proves the OpenAPI reference and generated client are in sync. Runs only when the API contract changed. | skipped, or ~3–5 min | | `storybook_visual` | Storybook build/behaviour/visual contracts for affected frontend projects, on cached Chromium. Runs beside `test`, not after it. | seconds when nothing affected; ~5 min otherwise | | `develop_smoke` | Boots a disposable k3d cluster, deploys the full current silo through the real deploy scripts, and proves database isolation, TLS ingress and workload health. | 6–15 min | | `image_smoke` | Boots individual images that declare an `image-smoke` target and checks they come up. | 1–2 min per image | @@ -69,7 +73,7 @@ Every job runs on a fresh runner, so anything not cached is paid on every run. | --- | --- | --- | --- | | npm download cache | `actions/setup-node` | lockfile hash | all jobs | | `node_modules` | `actions/cache` | lockfile hash | all jobs (skips `npm ci` entirely on a hit) | -| Nx computation cache | `actions/cache` (`.nx/cache`) | lockfile hash + commit, with prefix restore | `test`, `storybook_visual` | +| Nx computation cache | `actions/cache` (`.nx/cache`) | lockfile hash + commit, with prefix restore | `test`, `api_contract`, `storybook_visual` | | Playwright Chromium | `actions/cache` (`~/.cache/ms-playwright`) | lockfile hash | `storybook_visual` | | Docker image layers | registry (`ghcr.io//opencrane-buildcache:`) | buildx layer graph | `develop_smoke`, `build-and-push`, `publish-develop-smoke-images` | | npm inside Dockerfiles | BuildKit cache mount (`/root/.npm`) | shared between build and runtime stages within one build | all Node images | From b5c33040447a3816956c019812a0ed89d4ea1205 Mon Sep 17 00:00:00 2001 From: Jente Rosseel Date: Wed, 19 Aug 2026 16:06:40 +0300 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=90=9B=20skip=20the=20credential=20ro?= =?UTF-8?q?ll=20on=20fresh=20installs=20and=20widen=20the=20smoke=20timeou?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both the old unconditional restart and the checksum roll forced a second boot of the heaviest workloads on fresh installs, where nothing needs propagating: the pods start after this run published the Secrets. Since the public health report and initial model seeding grew the server's boot, that second roll stopped converging inside the blanket 300s wait on the loaded smoke runner. Fresh installs now skip the roll, and the smoke gets 600s of headroom that a healthy wait never uses. --- .github/workflows/docker.yml | 4 +++- apps/_infra/deploy-k8s/platform/k8s-deploy.sh | 24 ++++++++++++------- docs/ci-and-deploy.md | 6 +++-- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0a594d423..ed29cacac 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -554,7 +554,9 @@ jobs: run: ./apps/_infra/deploy-k8s/platform/tests/develop-smoke.sh env: KEEP_CLUSTER: "0" - TIMEOUT_SECONDS: "300" + # 600 gives the loaded two-CPU runner headroom: the server pod's boot grew with the + # public health report and initial model seeding, and a healthy wait returns early. + TIMEOUT_SECONDS: "600" SMOKE_AFFECTED_PROJECTS: ${{ needs.prepare.outputs.develop_smoke_projects }} SMOKE_BASE_SHA: ${{ needs.prepare.outputs.nx_base }} SMOKE_REGISTRY: ${{ env.REGISTRY }}/${{ github.repository_owner }} diff --git a/apps/_infra/deploy-k8s/platform/k8s-deploy.sh b/apps/_infra/deploy-k8s/platform/k8s-deploy.sh index 9da2a6c4e..5e4cd4c8b 100755 --- a/apps/_infra/deploy-k8s/platform/k8s-deploy.sh +++ b/apps/_infra/deploy-k8s/platform/k8s-deploy.sh @@ -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 @@ -1004,13 +1008,17 @@ run_opencrane_finalization_stage helm "${helm_args[@]}" || 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. -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 $? +# 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. diff --git a/docs/ci-and-deploy.md b/docs/ci-and-deploy.md index 4d63048cc..c4acdaeb9 100644 --- a/docs/ci-and-deploy.md +++ b/docs/ci-and-deploy.md @@ -131,8 +131,10 @@ flowchart TD restores the exact fenced Helm revision on failure. - After the umbrella upgrade, the engine stamps a checksum of the published database connection Secrets onto the consumer Deployments (`opencrane-server`, `litellm`, `mcp-gateway`). An - unchanged checksum is a no-op; a changed one triggers exactly one rollout. (This replaced an - unconditional `rollout restart` that double-started the heaviest workloads on every deploy.) + unchanged checksum is a no-op; a changed one triggers exactly one rollout; and a fresh install + skips the roll entirely because its pods were born after the Secrets were published. (This + replaced an unconditional `rollout restart` that double-started the heaviest workloads on + every deploy.) ## Warnings — read before deploying From 5696f9ea3a6ff2b34145d9fc1fe93b374c07d565 Mon Sep 17 00:00:00 2001 From: Jente Rosseel Date: Wed, 19 Aug 2026 16:27:15 +0300 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=90=9B=20assert=20the=20health=20repo?= =?UTF-8?q?rt=20the=20smoke=20can=20actually=20reach?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seeding a placeholder model provider (added while chasing the models probe) made the server fetch a BYOK Secret through the API server and exit fatally when that call failed on the loaded runner, turning one unavailable probe into a CrashLoopBackOff. CI holds no provider credentials, so the smoke stops pretending otherwise: it asserts the report is complete, every service it can provision is healthy, and model routing may be unavailable. A broken channel, memory, file, or database probe still fails the gate. --- .../platform/tests/develop-smoke.sh | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/_infra/deploy-k8s/platform/tests/develop-smoke.sh b/apps/_infra/deploy-k8s/platform/tests/develop-smoke.sh index d20927c0a..70ab44893 100755 --- a/apps/_infra/deploy-k8s/platform/tests/develop-smoke.sh +++ b/apps/_infra/deploy-k8s/platform/tests/develop-smoke.sh @@ -385,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" @@ -393,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 @@ -489,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" \ @@ -504,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" \