diff --git a/.env.example b/.env.example index b4c4775..0b64736 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,9 @@ POSTGRES_DB=crank POSTGRES_USER=crank POSTGRES_PASSWORD=change-me CRANK_DATABASE_URL=postgres://crank:change-me@postgres:5432/crank +CRANK_ADMIN_API_IMAGE=crank/admin-api:dev +CRANK_MCP_SERVER_IMAGE=crank/mcp-server:dev +CRANK_UI_IMAGE=crank/ui:dev CRANK_STORAGE_ROOT=/var/lib/crank/storage CRANK_PUBLISH_HOST=127.0.0.1 CRANK_ADMIN_BIND=0.0.0.0:3001 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 328aa98..4623f73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,7 +75,7 @@ jobs: run: docker build -f apps/ui/Dockerfile . deployment: - name: Deployment Artifacts + name: Deployment Manifests runs-on: ubuntu-latest steps: @@ -84,6 +84,3 @@ jobs: - name: Validate docker compose run: docker compose config -q - - - name: Build deployable images - run: docker compose build admin-api mcp-server ui diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4b784d5..2f6f991 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -12,8 +12,77 @@ concurrency: group: deploy-${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref }} cancel-in-progress: true +permissions: + contents: read + packages: write + +env: + IMAGE_TAG: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} + ADMIN_API_IMAGE: ghcr.io/${{ github.repository_owner }}/crank-admin-api + MCP_SERVER_IMAGE: ghcr.io/${{ github.repository_owner }}/crank-mcp-server + UI_IMAGE: ghcr.io/${{ github.repository_owner }}/crank-ui + jobs: + build-images: + if: > + github.event_name == 'workflow_dispatch' || + ( + github.event_name == 'workflow_run' && + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_branch == 'main' + ) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: admin-api + dockerfile: apps/admin-api/Dockerfile + - name: mcp-server + dockerfile: apps/mcp-server/Dockerfile + - name: ui + dockerfile: apps/ui/Dockerfile + + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} + + - name: Resolve image name + id: image + run: | + case "${{ matrix.name }}" in + admin-api) echo "name=${ADMIN_API_IMAGE}" >> "$GITHUB_OUTPUT" ;; + mcp-server) echo "name=${MCP_SERVER_IMAGE}" >> "$GITHUB_OUTPUT" ;; + ui) echo "name=${UI_IMAGE}" >> "$GITHUB_OUTPUT" ;; + *) exit 1 ;; + esac + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push ${{ matrix.name }} + uses: docker/build-push-action@v6 + with: + context: . + file: ${{ matrix.dockerfile }} + push: true + tags: | + ${{ steps.image.outputs.name }}:${{ env.IMAGE_TAG }} + ${{ steps.image.outputs.name }}:main + cache-from: type=gha,scope=${{ matrix.name }} + cache-to: type=gha,mode=max,scope=${{ matrix.name }} + deploy: + needs: build-images if: > github.event_name == 'workflow_dispatch' || ( @@ -41,28 +110,32 @@ jobs: printf '%s\n' "${{ secrets.DEPLOY_KNOWN_HOSTS }}" > ~/.ssh/known_hosts chmod 644 ~/.ssh/known_hosts - - name: Sync project to server + - name: Sync deployment files to server run: | - rsync -az --delete \ - --exclude ".git" \ - --exclude "target" \ - --exclude "notes" \ - --exclude "node_modules" \ - ./ "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/" + ssh "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \ + "mkdir -p '${{ secrets.DEPLOY_PATH }}'" + rsync -az docker-compose.yml \ + "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/docker-compose.yml" - name: Write environment file run: | - ssh "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \ - "mkdir -p '${{ secrets.DEPLOY_PATH }}' && cat > '${{ secrets.DEPLOY_PATH }}/.env'" \ - <<< "${{ secrets.DEPLOY_ENV_FILE }}" + { + printf '%s\n' "${{ secrets.DEPLOY_ENV_FILE }}" + printf 'CRANK_ADMIN_API_IMAGE=%s:%s\n' "${{ env.ADMIN_API_IMAGE }}" "${{ env.IMAGE_TAG }}" + printf 'CRANK_MCP_SERVER_IMAGE=%s:%s\n' "${{ env.MCP_SERVER_IMAGE }}" "${{ env.IMAGE_TAG }}" + printf 'CRANK_UI_IMAGE=%s:%s\n' "${{ env.UI_IMAGE }}" "${{ env.IMAGE_TAG }}" + } | ssh "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \ + "mkdir -p '${{ secrets.DEPLOY_PATH }}' && cat > '${{ secrets.DEPLOY_PATH }}/.env'" - name: Deploy with Docker Compose run: | ssh "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" " set -e cd '${{ secrets.DEPLOY_PATH }}' + echo '${{ secrets.DEPLOY_REGISTRY_TOKEN }}' | docker login ghcr.io -u '${{ secrets.DEPLOY_REGISTRY_USER }}' --password-stdin docker compose config -q - docker compose up -d --build --remove-orphans + docker compose pull admin-api mcp-server ui + docker compose up -d --remove-orphans " - name: Verify health endpoints diff --git a/README.md b/README.md index 9c6d41c..162e683 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,8 @@ Crank - платформа для публикации внешних API в в В репозитории настроены: -- `CI` для Rust, UI container и deployment artifacts; -- `CD`, который запускается после успешного `CI` на `main` или вручную; +- `CI` для Rust, UI и deployment manifests; +- `CD`, который после успешного `CI` на `main` собирает versioned images, пушит их в `GHCR` и деплоит через `docker compose pull`; - containerized deployment через `docker compose`. ## Поддерживаемые протоколы diff --git a/TASKS.md b/TASKS.md index c6d4d00..8980392 100644 --- a/TASKS.md +++ b/TASKS.md @@ -2,15 +2,15 @@ ## Current -### `feat/workspace-access-polish` +### `feat/cd-image-pipeline` Status: in_progress DoD: -- `workspace-setup` использует live backend для role-management, member removal, export и delete -- UI больше не показывает placeholder-поведение для workspace access lifecycle -- backend дает workspace export и delete контракты, согласованные с UI -- `test-ui` остается нетронутым как fallback +- CI больше не собирает deployable images без использования результата +- Deploy собирает и пушит versioned images в registry с cache +- production deploy использует `docker compose pull && docker compose up -d`, без server-side build +- docs и runtime config описывают registry-based rollout ## Next diff --git a/docker-compose.yml b/docker-compose.yml index e86e167..08d5e74 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,7 @@ services: retries: 5 admin-api: + image: ${CRANK_ADMIN_API_IMAGE:-crank/admin-api:dev} build: context: . dockerfile: apps/admin-api/Dockerfile @@ -48,6 +49,7 @@ services: retries: 5 mcp-server: + image: ${CRANK_MCP_SERVER_IMAGE:-crank/mcp-server:dev} build: context: . dockerfile: apps/mcp-server/Dockerfile @@ -74,6 +76,7 @@ services: retries: 5 ui: + image: ${CRANK_UI_IMAGE:-crank/ui:dev} build: context: . dockerfile: apps/ui/Dockerfile diff --git a/docs/deployment.md b/docs/deployment.md index b963c27..5b8a066 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -199,22 +199,37 @@ CI должен выполнять: CD для MVP должен: - запускаться только после успешного `CI` на `main` или вручную; -- собирать production artifacts; +- собирать versioned container images; +- пушить их в container registry с cache; - доставлять deployment files на целевой Linux host; -- выполнять controlled restart; +- выполнять controlled restart через `pull`, без сборки на сервере; - проверять health endpoints после запуска. Минимальный flow: 1. push в `main` 2. успешный `CI` -3. build images или удаленная сборка на хосте +3. build and push images в registry 4. upload env и deployment files -5. `docker compose config -q` -6. `docker compose up -d` -7. healthcheck verification для `ui`, `admin-api`, `mcp-server` +5. `docker login` на registry на целевом хосте +6. `docker compose config -q` +7. `docker compose pull` +8. `docker compose up -d` +9. healthcheck verification для `ui`, `admin-api`, `mcp-server` -Для MVP допустим deployment с удаленной сборкой на целевом Linux host через `ssh + rsync + docker compose up -d --build`, если не используется отдельный container registry. +Для текущего проекта базовая рекомендуемая схема: + +- `GHCR` как registry; +- versioned tags по `git sha`; +- `docker/buildx` cache в GitHub Actions; +- на сервере только `pull + restart`, без `docker compose up --build`. + +Для private registry на сервере нужны отдельные deployment secrets: + +- `DEPLOY_REGISTRY_USER` +- `DEPLOY_REGISTRY_TOKEN` + +Они используются только для `docker login` на целевом Linux host перед `docker compose pull`. ## 12. Что нельзя забыть diff --git a/docs/runtime-config.md b/docs/runtime-config.md index 0bc39f3..8245734 100644 --- a/docs/runtime-config.md +++ b/docs/runtime-config.md @@ -58,6 +58,9 @@ var/crank/ Минимально ожидаются: - `CRANK_DATABASE_URL` +- `CRANK_ADMIN_API_IMAGE` +- `CRANK_MCP_SERVER_IMAGE` +- `CRANK_UI_IMAGE` - `CRANK_STORAGE_ROOT` - `CRANK_ADMIN_BIND` - `CRANK_MCP_BIND` @@ -116,6 +119,7 @@ Demo/deployment: - включенная app-level auth-защита admin-api; - стабильный `Streamable HTTP` endpoint для MCP. - containerized runtime через `Docker` и `docker-compose`. +- registry-backed image rollout через `GHCR` или совместимый registry. ### Auth env