diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index 3b6b387..269f123 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -14,30 +14,29 @@ jobs: REGISTRY_HOST: ${{ vars.REGISTRY_HOST }} PROJECT_NAME: ${{ vars.PROJECT_NAME || 'unrip' }} PROJECT_NAMESPACE: ${{ vars.PROJECT_NAMESPACE || vars.PROJECT_NAME || 'unrip' }} - PROJECT_DEPLOYMENTS: ${{ vars.PROJECT_DEPLOYMENTS || 'near-intents-ingest,market-reference-ingest,liquidity-manager,inventory-sync,history-writer,strategy-engine,trade-executor' }} PROJECT_REGISTRY_SECRET_NAME: ${{ vars.PROJECT_REGISTRY_SECRET_NAME || format('{0}-registry-creds', vars.PROJECT_NAME || 'unrip') }} REPO_CLONE_URL: ${{ github.server_url }}/${{ github.repository }}.git steps: - name: Install tooling run: | - if command -v git >/dev/null 2>&1 && command -v kubectl >/dev/null 2>&1; then + if command -v git >/dev/null 2>&1 && command -v kubectl >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1; then exit 0 fi if command -v apk >/dev/null 2>&1; then - apk add --no-cache git kubectl + apk add --no-cache git kubectl python3 exit 0 fi if command -v apt-get >/dev/null 2>&1; then apt-get update - apt-get install -y git curl ca-certificates + apt-get install -y git curl ca-certificates python3 curl -fsSLo /usr/local/bin/kubectl "https://dl.k8s.io/release/$(curl -fsSL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" chmod +x /usr/local/bin/kubectl exit 0 fi - echo "missing git/kubectl and no supported package manager found" >&2 + echo "missing git/kubectl/python3 and no supported package manager found" >&2 exit 1 - name: Prepare workspace @@ -77,9 +76,9 @@ jobs: echo "BUILD_JOB=$BUILD_JOB" } >> "$GITHUB_ENV" - - name: Apply manifests + - name: Ensure namespace exists run: | - kubectl apply -k "$WORKSPACE_DIR/deploy/k8s/base" + kubectl apply -f "$WORKSPACE_DIR/deploy/k8s/base/namespace.yaml" - name: Build and push image in-cluster env: @@ -144,12 +143,16 @@ jobs: kubectl -n "$PROJECT_NAMESPACE" wait --for=condition=Complete --timeout=20m "job/$BUILD_JOB" kubectl -n "$PROJECT_NAMESPACE" logs "job/$BUILD_JOB" - - name: Roll deployments to new image + - name: Apply release manifests and wait for rollout run: | - printf '%s\n' "$PROJECT_DEPLOYMENTS" | tr ',' '\n' | while IFS= read -r deployment; do - deployment="$(echo "$deployment" | xargs)" - [ -n "$deployment" ] || continue + kubectl kustomize "$WORKSPACE_DIR/deploy/k8s/base" \ + | python3 "$WORKSPACE_DIR/scripts/deploy/render_release_manifest.py" --image "$IMAGE" \ + | kubectl apply -f - - kubectl -n "$PROJECT_NAMESPACE" set image "deployment/$deployment" app="$IMAGE" - kubectl -n "$PROJECT_NAMESPACE" rollout status "deployment/$deployment" --timeout=180s - done + kubectl -n "$PROJECT_NAMESPACE" get deployment \ + -l "app.kubernetes.io/part-of=$PROJECT_NAME" \ + -o name \ + | while IFS= read -r deployment; do + [ -n "$deployment" ] || continue + kubectl -n "$PROJECT_NAMESPACE" rollout status "$deployment" --timeout=180s + done diff --git a/AGENTS.md b/AGENTS.md index 486ccb0..97e03f8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,6 +52,9 @@ Read: - Do not expand scope beyond the active implementation proof or research charter. - No backlog generation instead of implementation. - No scaffolding ahead of demonstrated need. +- The repository must be fully wired so every service deploys automatically from a repo push. +- Manual deployment intervention is forbidden. +- Manual `kubectl` rollout, image patching, or other ad hoc production reconciliation is forbidden except when the user explicitly asks for emergency break-glass incident handling. - Quote collection and analytics are first-class from day one. They are not a later add-on. - Do not present scaffolding, dashboards, placeholders, or mock flows as product progress. - State assumptions before coding when the environment, venue, chain, or source behavior is uncertain. @@ -129,6 +132,7 @@ Real progress means the repository can do more of the active proof with validate - storing durable inventory, pricing, decision, and execution records - producing a real decision from live data - making a real execution attempt through repo-controlled code +- deploying all repo-owned services automatically from push-driven repo workflow without manual cluster intervention - proving blocked-path safety with explicit evidence Fake progress includes: diff --git a/scripts/deploy/bootstrap.sh b/scripts/deploy/bootstrap.sh index f498947..e6b6978 100755 --- a/scripts/deploy/bootstrap.sh +++ b/scripts/deploy/bootstrap.sh @@ -10,7 +10,7 @@ FORGEJO_REMOTE_NAME="${FORGEJO_REMOTE_NAME:-forgejo}" PROJECT_NAME="${PROJECT_NAME:-unrip}" PROJECT_NAMESPACE="${PROJECT_NAMESPACE:-$PROJECT_NAME}" -PROJECT_DEPLOYMENTS="${PROJECT_DEPLOYMENTS:-near-intents-ingest,market-reference-ingest,liquidity-manager,inventory-sync,history-writer,ops-sentinel,strategy-engine,trade-executor}" +PROJECT_DEPLOYMENTS="${PROJECT_DEPLOYMENTS:-near-intents-ingest,market-reference-ingest,liquidity-manager,inventory-sync,history-writer,ops-sentinel,strategy-engine,trade-executor,operator-dashboard}" PROJECT_REGISTRY_SECRET_NAME="${PROJECT_REGISTRY_SECRET_NAME:-${PROJECT_NAME}-registry-creds}" APP_SECRET_NAME="${APP_SECRET_NAME:-${PROJECT_NAME}-secrets}" SYNC_FORGEJO_REMOTE="${SYNC_FORGEJO_REMOTE:-1}" diff --git a/scripts/deploy/render_release_manifest.py b/scripts/deploy/render_release_manifest.py new file mode 100644 index 0000000..3a79727 --- /dev/null +++ b/scripts/deploy/render_release_manifest.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import sys + + +PLACEHOLDER_IMAGE = "ghcr.io/example/unrip:bootstrap" + + +def render_release_manifest(manifest: str, image: str) -> str: + return manifest.replace(PLACEHOLDER_IMAGE, image) + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Render a release manifest by replacing placeholder app images." + ) + parser.add_argument("--image", required=True, help="Fully qualified image reference to deploy.") + args = parser.parse_args() + + source = sys.stdin.read() + sys.stdout.write(render_release_manifest(source, args.image)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/render_release_manifest_test.py b/test/render_release_manifest_test.py new file mode 100644 index 0000000..0403d4e --- /dev/null +++ b/test/render_release_manifest_test.py @@ -0,0 +1,42 @@ +import unittest + +from scripts.deploy.render_release_manifest import render_release_manifest + + +class RenderReleaseManifestTest(unittest.TestCase): + def test_swaps_placeholder_images_for_release_image(self): + input_manifest = """ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: operator-dashboard +spec: + template: + spec: + containers: + - name: app + image: ghcr.io/example/unrip:bootstrap +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ops-sentinel +spec: + template: + spec: + containers: + - name: app + image: ghcr.io/example/unrip:bootstrap +""" + + output = render_release_manifest( + input_manifest, + "registry.example/unrip:abc123", + ) + + self.assertIn("image: registry.example/unrip:abc123", output) + self.assertNotIn("ghcr.io/example/unrip:bootstrap", output) + + +if __name__ == "__main__": + unittest.main()