From 5a30971dedd136e2951921bb9739db42fe973d64 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Sun, 20 Sep 2026 15:59:41 +0700 Subject: [PATCH] ci(deploy): Nix flake + GitHub Actions pipeline for hermyhq (Next.js 16 standalone) - flake.nix: build hermyhq via npm ci + next build (standalone), package .next/standalone + static + public behind /nix/var/nix/profiles/hermyhq - next.config.ts: output: 'standalone' (Nix deploy bundles self-contained server) - .github/workflows/deploy.yml: nix-installer (DeterminateSystems) + attic push pub:asepharyana + VPS nix-store realise (fallback ssh copy) + nix-env profile + systemctl restart hermyhq + GC cleanup --- .github/workflows/deploy.yml | 140 +++++++++++++++++++++++++++++++++++ flake.nix | 104 ++++++++++++++++++++++++++ next.config.ts | 2 +- 3 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 flake.nix diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..a13c149 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,140 @@ +name: Build & Deploy Hermy HQ (Nix + Attic) + +on: + push: + branches: [main] + workflow_dispatch: + +concurrency: + group: hermyhq-deploy + cancel-in-progress: false + +permissions: + contents: read + id-token: write + +env: + VPS_HOST: ${{ secrets.VPS_HOST }} + VPS_USER: ${{ secrets.VPS_USER }} + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v22 + with: + determinate: false + extra-conf: | + sandbox = false + accept-flake-config = true + extra-substituters = https://attic.asepharyana.my.id/asepharyana + extra-trusted-public-keys = asepharyana:zBpY6vNI1nDJ4mU6W4q880BB0JB1qb3gRKkO/tGSMiQ= + + - name: Cache Nix + uses: DeterminateSystems/magic-nix-cache-action@v14 + with: + use-flakehub: false + + - name: Build hermyhq + id: build + run: | + nix build .#hermyhq --impure --option sandbox false --print-build-logs + STORE_PATH=$(readlink result) + echo "store-path=$STORE_PATH" >> "$GITHUB_OUTPUT" + echo "Build OK hermyhq: $STORE_PATH" + + - name: Push to Attic cache + env: + ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }} + run: | + if [ -z "$ATTIC_TOKEN" ]; then + echo "ATTIC_TOKEN not set; skipping attic push" + exit 0 + fi + STORE_PATH="${{ steps.build.outputs.store-path }}" + ATTIC_DIR="/nix/store/fygyy3yk4rqdknxkiwkqambpnhyax0k4-attic-0.1.0" + ATTIC_BIN="" + if command -v attic >/dev/null 2>&1; then + ATTIC_BIN="$(command -v attic)" + elif nix-store --realise "$ATTIC_DIR" 2>/tmp/attic-bootstrap.err; then + echo "✅ Pulled attic client from attic cache (HTTPS substituter)" + ATTIC_BIN="$ATTIC_DIR/bin/attic" + else + echo "attic client unavailable on runner; skipping push (VPS will fall back)" + exit 0 + fi + mkdir -p "$HOME/.config/attic" + cat > "$HOME/.config/attic/config.toml" < ~/.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: Deploy hermyhq to VPS + run: | + STORE_PATH="${{ steps.build.outputs.store-path }}" + echo "=== Realising hermyhq: $STORE_PATH (substitute from attic, fallback ssh copy) ===" + if ssh "$VPS_USER@$VPS_HOST" "sudo /nix/var/nix/profiles/default/bin/nix-store --realise '$STORE_PATH'" 2>/dev/null; then + echo "Substituted hermyhq from Attic cache" + else + echo "Attic substitute failed; falling back to ssh copy" + nix copy --to "ssh://$VPS_USER@$VPS_HOST" "$STORE_PATH" + fi + + echo "=== Updating profile ===" + ssh "$VPS_USER@$VPS_HOST" "sudo /nix/var/nix/profiles/default/bin/nix-env --profile /nix/var/nix/profiles/hermyhq --set '$STORE_PATH'" + + echo "=== Restarting service ===" + ssh "$VPS_USER@$VPS_HOST" \ + "sudo systemctl daemon-reload && sudo systemctl restart hermyhq && for i in \$(seq 1 15); do state=\$(sudo systemctl is-active hermyhq 2>/dev/null || echo inactive); [ \"\$state\" = \"active\" ] && break; sleep 2; done; echo \"final-state=\$state\"; [ \"\$state\" = \"active\" ]" + echo "✅ hermyhq deployed" + + cleanup: + needs: build-and-deploy + if: always() + runs-on: ubuntu-latest + steps: + - name: Nix GC on VPS + env: + VPS_HOST: ${{ secrets.VPS_HOST }} + VPS_USER: ${{ secrets.VPS_USER }} + SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + run: | + mkdir -p ~/.ssh + echo "$SSH_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts 2>/dev/null + ssh "$VPS_USER@$VPS_HOST" "sudo /usr/local/bin/nix-gc-vps.sh" || echo "⚠️ Nix GC gagal (non-fatal)" diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2af291d --- /dev/null +++ b/flake.nix @@ -0,0 +1,104 @@ +{ + description = "Hermy HQ — Hermes mission control dashboard (Next.js 16 standalone, Nix build)"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachSystem [ "x86_64-linux" ] (system: + let + pkgs = import nixpkgs { inherit system; }; + + # Source filter: exclude build artifacts + heavy/irrelevant dirs + # (path: literals ignore .gitignore; filter explicitly). + filterSource = { dir, ignore }: builtins.path { + path = dir; + name = "source"; + filter = (path: type: let base = baseNameOf path; in !(builtins.elem base ignore)); + }; + src = filterSource { + dir = ./.; + ignore = [ ".next" "node_modules" "src-tauri" "data" "hermes-bridge" "scanner" ".git" ]; + }; + + nodejs = pkgs.nodejs_22; + + npmInstall = '' + export HOME=$TMPDIR/home + export npm_config_cache=$TMPDIR/npm-cache + mkdir -p $npm_config_cache + + # SSL/TLS certs (Nix sandbox lacks system CA bundle) + export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt + export NODE_EXTRA_CA_CERTS=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt + export GIT_SSL_CAINFO=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt + export NIX_SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt + + # prisma needs openssl at runtime for its engine + export CPPFLAGS="-I${pkgs.lib.getDev pkgs.openssl}/include" + export LDFLAGS="-L${pkgs.lib.getLib pkgs.openssl}/lib" + + npm ci --ignore-scripts 2>&1 + # prisma generate (postinstall normally, but --ignore-scripts skipped it) + npx prisma generate 2>&1 + ''; + + hermyhq = pkgs.stdenv.mkDerivation { + pname = "hermyhq"; + version = "1.0.0"; + + src = src; + + nativeBuildInputs = [ nodejs pkgs.cacert ]; + + buildPhase = npmInstall + '' + echo "=== Building Next.js SSR (standalone) ===" + export NEXT_TELEMETRY_DISABLED=1 + # NEXT_PUBLIC_ is baked at build time — the dashboard shows the owner name + export NEXT_PUBLIC_OWNER_NAME="''${NEXT_PUBLIC_OWNER_NAME:-MythEclipse}" + npx next build 2>&1 + ''; + + installPhase = '' + echo "=== Packaging standalone server ===" + mkdir -p $out/lib/hermyhq/standalone + cp -r .next/standalone/. $out/lib/hermyhq/standalone/ + mkdir -p $out/lib/hermyhq/standalone/.next + cp -r .next/static $out/lib/hermyhq/standalone/.next/static + cp -r public $out/lib/hermyhq/standalone/public 2>/dev/null || true + + # Remove dangling symlinks (pnpm/npm hoisted layout) + find $out/lib/hermyhq/standalone -type l \ + ! -exec test -e {} \; -delete 2>/dev/null || true + + mkdir -p $out/bin + printf '%s\n' \ + "#!${pkgs.runtimeShell}" \ + "cd $out/lib/hermyhq/standalone" \ + "export HOSTNAME=127.0.0.1" \ + "exec ${nodejs}/bin/node server.js" > $out/bin/hermyhq + chmod +x $out/bin/hermyhq + ''; + + meta = { + description = "Hermy HQ — Next.js 16 mission-control dashboard"; + platforms = pkgs.lib.platforms.linux; + }; + }; + + in { + packages = { + inherit hermyhq; + default = hermyhq; + }; + + devShells.default = pkgs.mkShell { + buildInputs = [ nodejs pkgs.cacert ]; + shellHook = '' + echo "Hermy HQ dev shell ready — node $(node --version)" + ''; + }; + }); +} diff --git a/next.config.ts b/next.config.ts index 72e0b8f..dd5d7eb 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,7 +2,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { // Vercel-compatible settings - output: undefined, // default — Vercel handles this automatically + output: "standalone", // Nix deploy bundles the standalone server images: { unoptimized: false, },