name: Build & Deploy (Nix) on: push: branches: [main] workflow_dispatch: concurrency: group: gmw-deploy cancel-in-progress: false permissions: contents: read id-token: write env: VPS_HOST: ${{ secrets.VPS_HOST }} VPS_USER: ${{ secrets.VPS_USER }} jobs: test: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 0 submodules: false - name: Setup Node uses: actions/setup-node@v4 with: node-version: 22 - name: Install pnpm run: corepack enable && corepack prepare pnpm@11 --activate - name: Install deps (backend) working-directory: services/backend run: pnpm install --ignore-scripts --no-frozen-lockfile - name: Typecheck + test (backend) working-directory: services/backend run: | ./node_modules/.bin/tsc --noEmit # e2e.test.ts requires a live backend (API_BASE) — run unit tests only ./node_modules/.bin/vitest run --exclude "src/e2e.test.ts" - name: Install deps (discord-gateway) working-directory: services/discord-gateway run: pnpm install --ignore-scripts --no-frozen-lockfile - name: Typecheck + test (discord-gateway) working-directory: services/discord-gateway run: | ./node_modules/.bin/tsc --noEmit ./node_modules/.bin/vitest run - name: Biome check (all services) run: | cd services/backend && ./node_modules/.bin/biome check src/ tests/ cd ../discord-gateway && ./node_modules/.bin/biome check src/ build-and-deploy: needs: test runs-on: ubuntu-latest strategy: fail-fast: false matrix: service: [backend, discord-gateway, proxy, frontend] steps: - name: Checkout uses: actions/checkout@v7 with: fetch-depth: 0 submodules: false - name: Install Nix uses: DeterminateSystems/nix-installer-action@v22 with: determinate: false extra-conf: | sandbox = false accept-flake-config = true # Attic binary cache as substituter on the runner: lets CI pull the # prebuilt attic client (and any cached deps/builds) over HTTPS, # no SSH round-trip needed. extra-substituters (NOT # extra-trusted-substituters) is required — Determinate Nix never # merges trusted-* substituters for nix-store CLI clients. extra-substituters = https://attic.asepharyana.my.id/gmw extra-trusted-public-keys = gmw:Fq2Anzuhkb+T/hftWnPcveHSi21/RzIgIOeG8pCJa88= # NOTE: nix-installer-action unconditionally injects # 'build-provenance-tags' into /etc/nix/nix.conf (a Determinate # Nix-only setting). With determinate:false the runner's upstream # nix warns 'unknown setting build-provenance-tags' on every # invocation — benign, cosmetic. Switching determinate:true would # silence it but changes the runner's nix flavor. - name: Cache Nix uses: DeterminateSystems/magic-nix-cache-action@v14 with: use-flakehub: false - name: Build ${{ matrix.service }} id: build run: | nix build .#${{ matrix.service }} --impure --option sandbox false --print-build-logs STORE_PATH=$(readlink result) echo "store-path=$STORE_PATH" >> "$GITHUB_OUTPUT" echo "Build OK ${{ matrix.service }}: $STORE_PATH" - name: Setup SSH key env: SSH_KEY: ${{ secrets.SSH_PRIVATE_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 # Push build result to Attic binary cache (attic.asepharyana.my.id) so # the VPS can substitute it instead of a single-stream `nix copy ssh://`. # # Fast path: push DIRECTLY from the runner to the public attic endpoint # (validated 2026-08-10: token auth over public HTTPS works without # Tailscale). This skips the ~794MB closure SSH copy to the VPS that # used to take 25+ minutes per new store path. # # The attic client is NOT in nixpkgs anymore and has no prebuilt # releases, so we pull the same prebuilt closure the VPS uses # (/nix/store/fygyy3yk4rqdknxkiwkqambpnhyax0k4-attic-0.1.0, ~52MB). # The closure itself lives in the attic cache (pushed once from the # VPS), so the runner bootstraps it over HTTPS via the configured # extra-substituters — no SSH round-trip. If that fails we fall back # to `nix copy --from ssh://`, then the old VPS-hop flow (SSH copy to # VPS, then attic push from the VPS over Tailscale) so the deploy step # always has a working closure 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="$ATTIC_DIR/bin/attic" attic_push_vps_hop() { echo "Fallback: VPS-hop attic push" # Copy closure to VPS (fast if attic already has it via substitute) ssh "$VPS_USER@$VPS_HOST" "sudo /nix/var/nix/profiles/default/bin/nix-store --realise '$STORE_PATH'" 2>/dev/null \ || nix copy --to "ssh://$VPS_USER@$VPS_HOST" "$STORE_PATH" # Push from VPS → Attic over Tailscale. # --ignore-upstream-cache-filter is REQUIRED: without it, attic skips # writing the narinfo to gmw when chunks exist in the upstream # cache.nixos.org — leaving the path 404 on gmw so the VPS deploy's # nix-store --realise can't find it and falls back to ssh copy. # sudo: attic must read root's config (~/.config/attic), which has # the imrnes-ts server → Tailscale. Non-root users' configs only # have the public `pub` server → "Server imrnes-ts does not exist". ssh "$VPS_USER@$VPS_HOST" "sudo $ATTIC_BIN push imrnes-ts:gmw '$STORE_PATH' --jobs 4 --ignore-upstream-cache-filter" \ || echo "attic push failed (non-fatal; ssh copy fallback below)" } # ── Get an attic client on the runner ──────────────────────────── # Order: PATH → pull the prebuilt closure from the attic cache # itself (extra-substituters configured in Install Nix step, HTTPS # only, no SSH) → pull over ssh from the VPS → VPS-hop. # The attic client closure is stored in the attic cache (pushed # once from the VPS), so the fast path never depends on SSH. 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" elif nix copy --from "ssh://$VPS_USER@$VPS_HOST" "$ATTIC_DIR" 2>>/tmp/attic-bootstrap.err; then echo "✅ Pulled attic client from VPS over ssh" ATTIC_BIN="$ATTIC_DIR/bin/attic" else echo "attic client unavailable on runner; using VPS-hop flow" echo "--- bootstrap errors (stderr) ---" tail -5 /tmp/attic-bootstrap.err 2>/dev/null || true attic_push_vps_hop exit 0 fi # ── Direct push: runner → attic public endpoint ────────────────── # --ignore-upstream-cache-filter forces the narinfo write even when # the path's chunks already exist in upstream cache.nixos.org (which # attic would otherwise skip, leaving the path 404 on the gmw cache). mkdir -p "$HOME/.config/attic" cat > "$HOME/.config/attic/config.toml" </dev/null; then echo "Substituted ${{ matrix.service }} 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/gmw-${{ matrix.service }} --set '$STORE_PATH'" echo "=== Restarting service ===\n" ssh "$VPS_USER@$VPS_HOST" \ "sudo systemctl daemon-reload && sudo systemctl restart gmw-${{ matrix.service }} && for i in \$(seq 1 15); do state=\$(sudo systemctl is-active gmw-${{ matrix.service }} 2>/dev/null || echo inactive); [ \"\$state\" = \"active\" ] && break; sleep 2; done; echo \"final-state=\$state\"; [ \"\$state\" = \"active\" ]" echo "✅ gmw-${{ matrix.service }} deployed" cleanup: # Bersihkan sampah Nix di VPS SETELAH semua deploy selesai: hapus generasi # profile lama + nix store gc. Profil yang sedang dipakai tidak disentuh. 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)"