From 5fdf5ffb645cc947e593b089680695bc51ea7185 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Fri, 21 Aug 2026 15:56:22 +0700 Subject: [PATCH] ci: Nix-based build + deploy for all services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - flake.nix: 4 packages (web, api, mcp, worker) built via bun in Nix - web.nix: pre-built .next packaged into Nix store (avoids Turbopack/Bun sandbox issue) - ci.yml: test (bun) → build-and-deploy (api/mcp/worker via nix build) + deploy-web (.next artifact → nix-build web.nix) - Deploy: nix copy → nix-env --set + systemctl restart (no tarball+SSH, no VPS builds) - systemd units updated to use Nix profile paths (/nix/var/nix/profiles/mcpedia-*) --- .github/workflows/ci.yml | 118 +++++++++++++++++++++++-- flake.nix | 184 +++++++++++++++++---------------------- web.nix | 59 +++++++++++++ 3 files changed, 254 insertions(+), 107 deletions(-) create mode 100644 web.nix diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72b2a96..83d9b3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,24 @@ jobs: - name: Test run: bun run test + # Build .next for web — done here because Next.js/Turbopack has a + # Bun incompatibility in the Nix sandbox. The web derivation packages + # this pre-built .next (see build-and-deploy matrix below). + - name: Build Next.js .next + working-directory: apps/web + run: bun run build + + # Upload .next for the web Nix derivation + - name: Tar .next + run: tar -C apps/web --exclude='.next/cache' -czf web-next.tar.gz .next + + - name: Upload web .next artifact + uses: actions/upload-artifact@v4 + with: + name: mcpedia-web-next + path: web-next.tar.gz + if-no-files-found: error + build-and-deploy: name: build + deploy (Nix) needs: test @@ -47,7 +65,7 @@ jobs: strategy: fail-fast: false matrix: - service: [web, api, mcp, worker] + service: [api, mcp, worker] steps: - name: Checkout @@ -95,10 +113,8 @@ jobs: SERVICE="${{ matrix.service }}" PROFILE="mcpedia-${SERVICE}" - # Copy Nix closure to VPS nix copy --to "ssh://***@$VPS_HOST" "$STORE_PATH" - # Deploy: set profile + restart ssh "$VPS_USER@$VPS_HOST" \ "sudo /nix/var/nix/profiles/default/bin/nix-env \ --profile /nix/var/nix/profiles/$PROFILE \ @@ -108,8 +124,100 @@ jobs: sudo systemctl status $PROFILE --no-pager --no-legend | head -5" - name: Verify service + env: + SSH_KEY: ${{ secrets.SSH_DEPLOY_KEY }} run: | SERVICE="${{ matrix.service }}" - PORT_MAP="web:4016 api:4017 mcp:4021 worker:4018" + PORT_MAP="api:4017 mcp:4021 worker:4018" PORT=$(echo "$PORT_MAP" | grep "$SERVICE" | awk -F: '{print $2}') - ssh "$VPS_USER@$VPS_HOST" "curl -s -o /dev/null -w '%{http_code}' http://localhost:$PORT/health 2>/dev/null || systemctl is-active $PROFILE" + ssh "$VPS_USER@$VPS_HOST" \ + "curl -s -o /dev/null -w '%{http_code}' http://localhost:$PORT/health 2>/dev/null \ + || systemctl is-active $PROFILE" + + deploy-web: + name: build + deploy (web) + needs: test + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v16 + with: + determinate: false + extra-conf: | + sandbox = false + accept-flake-config = true + + - name: Cache Nix + uses: DeterminateSystems/magic-nix-cache-action@v14 + with: + use-flakehub: false + + - name: Download web .next artifact + uses: actions/download-artifact@v4 + with: + name: mcpedia-web-next + path: /tmp/web-next + + - name: Unpack .next + run: | + mkdir -p apps/web/.next + tar -xzf /tmp/web-next/web-next.tar.gz -C apps/web/ + + # Full monorepo install (needed for node_modules + workspace symlinks) + - name: Install deps + run: bun install --frozen-lockfile + + - name: Build web Nix derivation + id: build + run: | + nix-build -E \ + "let pkgs = import {}; in pkgs.callPackage ./web.nix { + webNextDir = /home/runner/work/mcpedia/mcpedia/apps/web/.next; + webPkgDir = /home/runner/work/mcpedia/mcpedia/apps/web; + nodeModulesPath = /home/runner/work/mcpedia/mcpedia; + }" \ + --impure --option sandbox false + STORE_PATH=$(readlink result) + echo "store-path=$STORE_PATH" >> "$GITHUB_OUTPUT" + echo "Build OK web: $STORE_PATH" + + - name: Setup SSH key + env: + SSH_KEY: ${{ secrets.SSH_DEPLOY_KEY }} + run: | + mkdir -p ~/.ssh + echo "$SSH_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + sed -i 's/\r$//' ~/.ssh/id_ed25519 + ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null 2>&1 || { echo "SSH key invalid"; exit 1; } + ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts 2>/dev/null + + - name: Copy to VPS & deploy + env: + SSH_KEY: ${{ secrets.SSH_DEPLOY_KEY }} + run: | + STORE_PATH="${{ steps.build.outputs.store-path }}" + PROFILE="mcpedia-web" + + nix copy --to "ssh://***@$VPS_HOST" "$STORE_PATH" + + ssh "$VPS_USER@$VPS_HOST" \ + "sudo /nix/var/nix/profiles/default/bin/nix-env \ + --profile /nix/var/nix/profiles/$PROFILE \ + --set '$STORE_PATH' && \ + sudo systemctl restart $PROFILE && \ + sleep 3 && \ + sudo systemctl status $PROFILE --no-pager --no-legend | head -5" + + - name: Verify public URL + env: + SSH_KEY: ${{ secrets.SSH_DEPLOY_KEY }} + run: | + ssh "$VPS_USER@$VPS_HOST" \ + "curl -sk -o /dev/null -w '%{http_code}' https://wiki.asepharyana.my.id/" diff --git a/flake.nix b/flake.nix index 7890fe1..28fe5e0 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "MCPedia — Nix-native build for web, api, mcp, worker services"; + description = "MCPedia — Nix-native build for api, mcp, worker. Web .next pre-built in CI."; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; @@ -8,108 +8,88 @@ outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: - let - pkgs = import nixpkgs { inherit system; }; - - # ─── Web: Next.js ─────────────────────────────────────────────── - web = pkgs.stdenvNoCC.mkDerivation { - pname = "mcpedia-web"; - version = "1.0.0"; - src = ./.; - nativeBuildInputs = [ pkgs.bun pkgs.makeBinaryWrapper ]; - buildPhase = '' - export HOME=$TMPDIR - bun install --frozen-lockfile 2>&1 - cd apps/web - bun run build - ''; - installPhase = '' - mkdir -p $out/share/mcpedia-web - cp -rL apps/web/.next $out/share/mcpedia-web/.next - cp -rL apps/web/node_modules $out/share/mcpedia-web/node_modules - cp apps/web/package.json $out/share/mcpedia-web/ - makeBinaryWrapper ${pkgs.bun}/bin/bun $out/bin/mcpedia-web \ - --add-flags "node_modules/.bin/next start" \ - --chdir $out/share/mcpedia-web - ''; - }; - - # ─── API: Hono ────────────────────────────────────────────────── - api = pkgs.stdenvNoCC.mkDerivation { - pname = "mcpedia-api"; - version = "1.0.0"; - src = ./.; - nativeBuildInputs = [ pkgs.bun pkgs.makeBinaryWrapper ]; - buildPhase = '' - export HOME=$TMPDIR - bun install --frozen-lockfile 2>&1 - cd apps/api - bun build src/index.ts --outdir dist --external @mcpedia/* - ''; - installPhase = '' - mkdir -p $out/share/mcpedia-api - cp -rL apps/api/dist $out/share/mcpedia-api/dist - cp -rL apps/api/node_modules $out/share/mcpedia-api/node_modules - cp apps/api/package.json $out/share/mcpedia-api/ - makeBinaryWrapper ${pkgs.bun}/bin/bun $out/bin/mcpedia-api \ - --add-flags "run dist/index.js" \ - --chdir $out/share/mcpedia-api - ''; - }; - - # ─── MCP: Streamable HTTP ─────────────────────────────────────── - mcp = pkgs.stdenvNoCC.mkDerivation { - pname = "mcpedia-mcp"; - version = "1.0.0"; - src = ./.; - nativeBuildInputs = [ pkgs.bun pkgs.makeBinaryWrapper ]; - buildPhase = '' - export HOME=$TMPDIR - bun install --frozen-lockfile 2>&1 - cd apps/mcp - bun build src/http.ts --outdir dist --external @mcpedia/* --external @modelcontextprotocol/* - ''; - installPhase = '' - mkdir -p $out/share/mcpedia-mcp - cp -rL apps/mcp/dist $out/share/mcpedia-mcp/dist - cp -rL apps/mcp/node_modules $out/share/mcpedia-mcp/node_modules - cp apps/mcp/package.json $out/share/mcpedia-mcp/ - makeBinaryWrapper ${pkgs.bun}/bin/bun $out/bin/mcpedia-mcp \ - --add-flags "run dist/http.js" \ - --chdir $out/share/mcpedia-mcp - ''; - }; - - # ─── Worker: BullMQ ───────────────────────────────────────────── - worker = pkgs.stdenvNoCC.mkDerivation { - pname = "mcpedia-worker"; - version = "1.0.0"; - src = ./.; - nativeBuildInputs = [ pkgs.bun pkgs.makeBinaryWrapper ]; - buildPhase = '' - export HOME=$TMPDIR - bun install --frozen-lockfile 2>&1 - cd apps/worker - bun build src/index.ts --outdir dist --external @mcpedia/* - ''; - installPhase = '' - mkdir -p $out/share/mcpedia-worker - cp -rL apps/worker/dist $out/share/mcpedia-worker/dist - cp -rL apps/worker/node_modules $out/share/mcpedia-worker/node_modules - cp apps/worker/package.json $out/share/mcpedia-worker/ - makeBinaryWrapper ${pkgs.bun}/bin/bun $out/bin/mcpedia-worker \ - --add-flags "run dist/index.js" \ - --chdir $out/share/mcpedia-worker - ''; - }; - - in { + let pkgs = import nixpkgs { inherit system; }; in { packages = { - web = web; - api = api; - mcp = mcp; - worker = worker; - default = web; # `nix build` builds web by default + api = pkgs.stdenvNoCC.mkDerivation { + pname = "mcpedia-api"; + version = "1.0.0"; + src = ./.; + nativeBuildInputs = [ pkgs.bun pkgs.makeBinaryWrapper ]; + buildPhase = '' + export HOME=$TMPDIR + bun install --frozen-lockfile 2>&1 + bun build apps/api/src/index.ts \ + --outdir apps/api/dist \ + --target bun \ + --external @mcpedia/config \ + --external @mcpedia/core \ + --external @mcpedia/db \ + --external @mcpedia/queue + ''; + installPhase = '' + mkdir -p $out/share/mcpedia-api + cp -r apps/api/dist $out/share/mcpedia-api/dist + cp -rL apps/api/node_modules $out/share/mcpedia-api/node_modules + cp apps/api/package.json $out/share/mcpedia-api/ + makeBinaryWrapper ${pkgs.bun}/bin/bun $out/bin/mcpedia-api \ + --add-flags "run dist/index.js" \ + --chdir $out/share/mcpedia-api + ''; + }; + + mcp = pkgs.stdenvNoCC.mkDerivation { + pname = "mcpedia-mcp"; + version = "1.0.0"; + src = ./.; + nativeBuildInputs = [ pkgs.bun pkgs.makeBinaryWrapper ]; + buildPhase = '' + export HOME=$TMPDIR + bun install --frozen-lockfile 2>&1 + bun build apps/mcp/src/http.ts \ + --outdir apps/mcp/dist \ + --target bun \ + --external @mcpedia/config \ + --external @mcpedia/core \ + --external @mcpedia/queue \ + --external @modelcontextprotocol/sdk + ''; + installPhase = '' + mkdir -p $out/share/mcpedia-mcp + cp -r apps/mcp/dist $out/share/mcpedia-mcp/dist + cp -rL apps/mcp/node_modules $out/share/mcpedia-mcp/node_modules + cp apps/mcp/package.json $out/share/mcpedia-mcp/ + makeBinaryWrapper ${pkgs.bun}/bin/bun $out/bin/mcpedia-mcp \ + --add-flags "run dist/http.js" \ + --chdir $out/share/mcpedia-mcp + ''; + }; + + worker = pkgs.stdenvNoCC.mkDerivation { + pname = "mcpedia-worker"; + version = "1.0.0"; + src = ./.; + nativeBuildInputs = [ pkgs.bun pkgs.makeBinaryWrapper ]; + buildPhase = '' + export HOME=$TMPDIR + bun install --frozen-lockfile 2>&1 + bun build apps/worker/src/index.ts \ + --outdir apps/worker/dist \ + --target bun \ + --external @mcpedia/config \ + --external @mcpedia/core \ + --external @mcpedia/db \ + --external @mcpedia/queue + ''; + installPhase = '' + mkdir -p $out/share/mcpedia-worker + cp -r apps/worker/dist $out/share/mcpedia-worker/dist + cp -rL apps/worker/node_modules $out/share/mcpedia-worker/node_modules + cp apps/worker/package.json $out/share/mcpedia-worker/ + makeBinaryWrapper ${pkgs.bun}/bin/bun $out/bin/mcpedia-worker \ + --add-flags "run dist/index.js" \ + --chdir $out/share/mcpedia-worker + ''; + }; }; }); } diff --git a/web.nix b/web.nix new file mode 100644 index 0000000..1f0a640 --- /dev/null +++ b/web.nix @@ -0,0 +1,59 @@ +# ─── Web: packages pre-built .next ────────────────────────────────────── +# Next.js 16/Turbopack has a Bun incompatibility in the Nix sandbox. +# Fix: CI builds .next with `bun run build`, then this derivation packages +# the .next + node_modules (from a full monorepo install) into a Nix store path. +# +# `webNextDir` = Nix path to pre-built .next directory +# `webPkgDir` = Nix path to apps/web +# `nodeModulesPath` = Nix path to monorepo node_modules (root) +# +# Usage: +# nix-build -E 'let pkgs = import {}; in pkgs.callPackage ./web.nix { webNextDir = /abs/apps/web/.next; webPkgDir = /abs/apps/web; nodeModulesPath = /abs; }' + +{ pkgs +, stdenv +, makeBinaryWrapper +, bun +, webNextDir +, webPkgDir +, nodeModulesPath +, ... +}: + +stdenv.mkDerivation { + pname = "mcpedia-web"; + version = "1.0.0"; + src = pkgs.writeText "dummy-src" ""; + nativeBuildInputs = [ pkgs.bun makeBinaryWrapper ]; + dontUnpack = true; + buildPhase = ""; + installPhase = '' + mkdir -p $out/share/mcpedia-web + + # Copy pre-built .next + cp -r "${webNextDir}" $out/share/mcpedia-web/.next + + # Copy node_modules from monorepo root (preserves symlinks to workspace pkgs) + cp -r "${nodeModulesPath}/node_modules" $out/share/mcpedia-web/node_modules + + # Fix workspace symlinks: node_modules/@mcpedia/* points to ../../packages/* + # In the Nix store, resolve to actual package source dirs + cd $out/share/mcpedia-web + for ws_pkg in @mcpedia/config @mcpedia/core @mcpedia/db @mcpedia/embeddings \ + @mcpedia/parser @mcpedia/queue @mcpedia/search @mcpedia/types; do + link="node_modules/$ws_pkg" + if [ -L "$link" ]; then + target=$(readlink -f "$link") + rm "$link" + mkdir -p "$(dirname "$link")" + cp -rL "$target" "$link" + fi + done + + cp "${webPkgDir}/package.json" $out/share/mcpedia-web/ + + makeBinaryWrapper ${pkgs.bun}/bin/bun $out/bin/mcpedia-web \ + --add-flags "run node_modules/.bin/next start" \ + --chdir $out/share/mcpedia-web + ''; +}