42 Commits
Author SHA1 Message Date
monrepo-bot eb19385ab4 chore: update hub to ed9fa16b5760 2026-08-17 00:18:19 +00:00
monrepo-bot a1c0ef25ef chore: update hub to 1640230c66d3 2026-08-17 00:18:09 +00:00
monrepo-bot 609da6b6bc chore: update hub to c891439857df 2026-08-14 00:14:22 +00:00
monrepo-bot 402499d109 chore: update hub to bfbfc175ca82 2026-08-13 00:13:26 +00:00
monrepo-bot 1389d5faa3 chore: update hub to b30e6936b1df 2026-08-11 00:12:50 +00:00
monrepo-bot 59e6c5c6a1 chore: update hub to 9ea002a7a10d 2026-08-10 00:24:27 +00:00
monrepo-bot 4f8a975337 chore: update hub to e67db071bb0f 2026-08-10 00:24:15 +00:00
monrepo-bot c251867f05 chore: update hub to 17a785c9d277 2026-08-10 00:18:32 +00:00
monrepo-bot b06cddca4a chore: update hub to 915d8482947a 2026-08-10 00:18:27 +00:00
monrepo-bot 19aface599 chore: update hub to a5bf846e52fd 2026-08-10 00:18:19 +00:00
monrepo-bot 528a109d53 chore: update hub to eb80d4a0fd73 2026-08-07 00:10:49 +00:00
asepharyana 97b635e8c6 firewall: whitelist TCPShield proxy ranges untuk port 25565 (Minecraft) 2026-08-06 20:42:39 +07:00
mytheclipsebotreview[bot] 99e438d9c5 Auto-merge PR #10
chore(deps): bump actions/checkout from 6 to 7 in the github-actions group
2026-08-06 13:28:09 +00:00
dependabot[bot] 8e850f05c4 chore(deps): bump actions/checkout in the github-actions group
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-06 13:16:00 +00:00
asepharyana 6c2a4fc9d2 fix(infra): sync Caddyfile.prod with live — add dashboard.asepharyana.my.id block
dashboard was proxied via wildcard but missing from live Caddy site
blocks -> Cloudflare 525. Live /etc/caddy/Caddyfile is now the
authoritative copy; repo reference updated to match byte-for-byte so
the drift cannot recur.
2026-08-05 09:44:26 +07:00
Asep Haryana 3e92f68d78 fix(infra): revert imphnen block ke gmw-proxy 4009 (dimentorin gak dipublish via domain ini) 2026-08-04 23:12:02 +07:00
Asep Haryana 2050129062 fix(infra): imphnen site block — SPA :5173 + API /v1 → :4099 (was 525: no block on live caddy) 2026-08-04 22:51:09 +07:00
asepharyana a6148753ca fix(infra): 9router Caddy timeouts — false 504 on slow LLM TTFT
9router combo models (deepseek-v4-flash-free fallback) have TTFT up to
30-40s. Default (proxy) response_header_timeout 30s / read 60s caused
false-positive 504 'timeout awaiting response headers' even while 9router
was processing. Give the 9router route dedicated long timeouts
(response_header 120s, read/write 300s). Syncs /etc/caddy/Caddyfile.
2026-08-04 22:02:09 +07:00
asepharyana d02989f1a7 ci: add Nix GC cleanup job on VPS after deploy 2026-08-04 13:58:10 +07:00
monrepo-bot 6e317eb05a chore: update hub to 55310768a5ac 2026-08-04 02:39:29 +00:00
asepharyana 4e114edb89 chore: update settings.json with new hooks and commands; update .gitignore to include .serena/ directory 2026-08-03 16:49:28 +07:00
aseph 49123c08b3 ci: use free GHA Nix cache (disable FlakeHub cache, not subscribed) 2026-08-03 16:44:32 +07:00
asepharyana 539b5b8320 ci: enable FlakeHub Cache (id-token: write + use-flakehub) 2026-08-03 16:24:48 +07:00
monrepo-bot 39df52aab7 chore: update hub to ad6b1847c563 2026-08-03 09:23:16 +00:00
Asep Haryana b326911478 chore(infra): bump hub flake rev to a90d0c4 (error rate fix) 2026-08-03 13:07:38 +07:00
monrepo-bot 8242325ffd chore: update hub to a90d0c43336a 2026-08-03 06:06:50 +00:00
Asep Haryana c075cf282b fix(infra): correct hub flake rev to 62966fd (full sha) 2026-08-03 12:48:14 +07:00
Asep Haryana 93936efd40 chore(infra): bump hub flake rev to 62966fd (LLM dashboard card) 2026-08-03 12:46:11 +07:00
monrepo-bot 5f66cfc12b chore: update hub to 62966fd7cf45 2026-08-03 05:45:45 +00:00
Asep Haryana cc8c3f1bf3 chore(infra): add llm-api to prometheus targets.yml (now exposes /metrics) 2026-08-03 11:56:35 +07:00
Asep Haryana 0c36007da4 chore(infra): bump llm-api flake rev to 5f7ead5 2026-08-03 11:46:08 +07:00
monrepo-bot b7d02647d9 chore: update llm-api to 5f7ead550308 2026-08-03 04:45:04 +00:00
Asep HaryanaandClaude Opus 5 ade2798ded chore(infra): bump llm-api flake rev to 7cec411
Fixes streaming reasoning/content split (robust boundary detection,
flush un-tagged output) and renders reasoning in the chat UI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 11:04:18 +07:00
monrepo-bot ef14ca431f chore: update llm-api to 7cec411cba7c 2026-08-03 04:04:04 +00:00
Asep HaryanaandClaude Opus 5 f681e624b3 chore(infra): bump llm-api flake rev to b636496
Deploys the generation-flow refactor (unified generate, streaming fixes,
tool-calling, strict model validation, AddBos::Never).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 09:50:23 +07:00
Asep HaryanaandClaude Opus 5 fdbdcbc1ec fix(ci): trigger nix deploy on every push to main
GitHub path filters do not match submodule gitlink changes, so the
`paths: apps/**` filter meant a submodule pointer update never triggered
the deploy. Drop the filter so any push to main deploys (matches the
documented "Push to main -> nix build -> systemctl restart").

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 09:35:56 +07:00
Asep HaryanaandClaude Opus 5 b7b60e9125 chore(ci): remove legacy Docker workflows (moved to Nix)
Docker was decommissioned 2026-08-02 but docker-build-push.yml and
deploy-docker.yml were left behind. They still listened to
repository_dispatch: [submodule-updated], so every app push queued a
redundant Docker build alongside the intended Nix deploy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 09:13:38 +07:00
monrepo-bot 722709f6fb chore: update llm-api to b63649649758 2026-08-03 01:58:50 +00:00
Asep Haryana d20643fbbd fix(infra): ship next.config.ts into Nix store for hub
next start re-reads next.config.ts at runtime; the OLD store had only
.next/public/package.json/node_modules, so images.maximumDiskCacheSize
fell back to defaults (undefined) and the image-optimizer disk LRU did
mkdir(<store>/.next/cache/images) -> EACCES on every optimized image.
Also now that next.config.ts sets maximumDiskCacheSize:0 +
isrFlushToDisk:false, shipping the file makes runtime match the baked
build config, eliminating the EACCES spike.
2026-08-03 04:33:46 +07:00
Asep Haryana 7da1727603 fix(firewall): accept IPv6 MLD/ND multicast (ff02::1, ff02::2, ff02::fb)
Neighbor multicast to ff02::1 (MLDv2 reports from other hosts) was
hitting the LOG+DROP tail — ~1800 FW6-DROP lines/6h in journald,
i.e. ~5/min of pure log spam from ICMPv6 layer-2 discovery. Accept
link-local multicast ranges before the logging rule.
2026-08-03 04:06:32 +07:00
Asep Haryana 9e9fd229d0 chore(infra): bump hub flake rev to 6829c7e (image disk cache fix) 2026-08-03 04:05:23 +07:00
Asep Haryana 40095f75f4 chore(infra): bump hub submodule to 6829c7efe2a735d249f5135bff4a0fc28411a295 (disable Next image disk cache) 2026-08-03 04:05:15 +07:00
13 changed files with 145 additions and 610 deletions
+45 -1
View File
@@ -25,5 +25,49 @@
"filePattern": ".claude/skills/deploy-workflow.md",
"description": "CI/CD pipeline, Docker patterns, deployment guide"
}
]
],
"hooks": {
"PreToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "serena-hooks remind --client=claude-code"
}
]
},
{
"matcher": "mcp__serena__*",
"hooks": [
{
"type": "command",
"command": "serena-hooks auto-approve --client=claude-code"
}
]
}
],
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "serena-hooks activate --client=claude-code"
}
]
}
],
"SessionEnd": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "serena-hooks cleanup --client=claude-code"
}
]
}
]
}
}
-239
View File
@@ -1,239 +0,0 @@
name: Deploy Docker to VPS
on:
workflow_run:
workflows: ['Build and Push Docker Images']
types:
- completed
branches:
- main
push:
branches:
- main
paths:
- 'infra/**'
- '.github/workflows/deploy-docker.yml'
- '.github/workflows/docker-build-push.yml'
workflow_dispatch:
# Prevent multiple deployments from running simultaneously
concurrency:
group: deploy-vps
cancel-in-progress: false
permissions:
contents: read
packages: read
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 30
if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' || github.event.workflow_run.conclusion == 'success'
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 1
submodules: false
- name: Deploy to VPS
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
VPS_HOST: ${{ secrets.VPS_HOST }}
VPS_USER: ${{ secrets.VPS_USER }}
VPS_TARGET_DIR: ${{ secrets.VPS_TARGET_DIR }}
ENV_FILE_PRODUCTION: ${{ secrets.ENV_FILE_PRODUCTION }}
GHCR_USERNAME: ${{ github.actor }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
echo "Deploy event: ${{ github.event_name }}"
echo "Deploy ref: ${{ github.ref }}"
echo "Deploy sha: ${{ github.sha }}"
if [ -z "${SSH_PRIVATE_KEY:-}" ] || [ -z "${VPS_HOST:-}" ] || [ -z "${VPS_USER:-}" ] || [ -z "${VPS_TARGET_DIR:-}" ]; then
echo "❌ Deployment secrets are not fully configured. Please set SSH_PRIVATE_KEY, VPS_HOST, VPS_USER, and VPS_TARGET_DIR."
exit 1
fi
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H -t ed25519,rsa "$VPS_HOST" >> ~/.ssh/known_hosts
# Use SSH multiplexing for faster subsequent commands
SSH_OPTS=(-o ControlMaster=auto -o ControlPath=/tmp/ssh-%r@%h:%p -o ControlPersist=600 -o StrictHostKeyChecking=yes)
ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" "mkdir -p $VPS_TARGET_DIR && mkdir -p $VPS_TARGET_DIR/infra/compose"
echo "$ENV_FILE_PRODUCTION" > .env.prod
scp "${SSH_OPTS[@]}" .env.prod "$VPS_USER@$VPS_HOST:$VPS_TARGET_DIR/.env"
echo "🔐 Logging in to GitHub Container Registry..."
printf '%s' "$GHCR_TOKEN" | ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" "docker login ghcr.io -u '$GHCR_USERNAME' --password-stdin"
ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" "export VPS_TARGET_DIR=$VPS_TARGET_DIR; bash -s" <<'EOF'
set -euo pipefail
cd "$VPS_TARGET_DIR"
# Ensure shared network exists
docker network inspect app-shared-net >/dev/null 2>&1 || docker network create app-shared-net
echo "🔄 Synchronizing repository..."
if [ ! -d ".git" ]; then
echo "Initializing git repository..."
git init
git remote add origin https://github.com/asepharyana/asepharyana-hub.git
fi
git fetch origin main --depth=1 || true
# Detect changed files before resetting
ALL_COMPOSE_FILES="infra/compose/traefik.yml infra/compose/shared.yml infra/compose/scraper.yml infra/compose/hub.yml infra/compose/tools.yml infra/compose/llm-api.yml infra/compose/nats.yml infra/compose/dapr.yml infra/compose/observability.yml"
TRAEFIK_DYNAMIC_DIR="infra/traefik/dynamic"
if git rev-parse HEAD >/dev/null 2>&1; then
BEFORE_REV=$(git rev-parse HEAD)
git reset --hard FETCH_HEAD
AFTER_REV=$(git rev-parse HEAD)
if [ "$BEFORE_REV" = "$AFTER_REV" ]; then
echo "️ No new commits detected. Using full file list for safety."
TARGET_COMPOSE=""
else
CHANGED=$(git diff --name-only "$BEFORE_REV" "$AFTER_REV" || true)
echo "📄 Changed files:"
echo "$CHANGED"
# Detect compose stack changes
CHANGED_COMPOSE=$(echo "$CHANGED" | grep '^infra/compose/.*\.yml$' || true)
TARGET_COMPOSE=""
for f in $CHANGED_COMPOSE; do
case " $ALL_COMPOSE_FILES " in
*" $f "*) TARGET_COMPOSE="$TARGET_COMPOSE $f" ;;
esac
done
TARGET_COMPOSE=$(printf '%s' "$TARGET_COMPOSE" | xargs || true)
if [ -n "$TARGET_COMPOSE" ]; then
echo "🎯 Detected compose stack changes in: $TARGET_COMPOSE"
else
echo "️ No stack compose files changed."
fi
# Detect Traefik dynamic config changes
CHANGED_TRAEFIK=$(echo "$CHANGED" | grep "^$TRAEFIK_DYNAMIC_DIR/" || true)
if [ -n "$CHANGED_TRAEFIK" ]; then
echo "🎯 Detected Traefik dynamic config changes:"
echo "$CHANGED_TRAEFIK"
RELOAD_TRAEFIK="true"
else
echo "️ No Traefik dynamic config changes."
fi
# Detect infra file changes (Dockerfiles, config, traefik static)
CHANGED_INFRA=$(echo "$CHANGED" | grep '^infra/' | grep -v '^infra/compose/' || true)
if [ -n "$CHANGED_INFRA" ]; then
echo "📦 Detected other infra file changes:"
echo "$CHANGED_INFRA"
fi
fi
else
git reset --hard FETCH_HEAD
TARGET_COMPOSE=""
fi
if command -v "docker" >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
COMPOSE_CMD="docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD="docker-compose"
else
echo "❌ docker compose is not installed on the remote host."
exit 1
fi
# Always include ALL compose files for dependency resolution
COMPOSE_ARGS=""
for f in $ALL_COMPOSE_FILES; do
if [ -f "$f" ]; then
COMPOSE_ARGS="$COMPOSE_ARGS -f $f"
fi
done
if [ -n "$TARGET_COMPOSE" ]; then
# Extract service names from target compose file(s) for selective up
TARGET_SERVICES=""
for f in $TARGET_COMPOSE; do
if [ -f "$f" ]; then
svcs=$($COMPOSE_CMD -f "$f" config --services 2>/dev/null | tr '\n' ' ' | xargs)
TARGET_SERVICES="$TARGET_SERVICES $svcs"
fi
done
TARGET_SERVICES=$(echo "$TARGET_SERVICES" | xargs) # trim whitespace
echo "🎯 Selective update for services: $TARGET_SERVICES"
else
echo "🚀 Performing full deployment of all services..."
TARGET_SERVICES=""
fi
echo "📥 Pulling images for target services..."
export DOCKER_CLI_EXPERIMENTAL=enabled
PULL_SUCCESS=false
# Retry pull up to 3 times to handle transient Docker attestation lease errors
for attempt in 1 2 3; do
echo "Pull attempt $attempt/3..."
if $COMPOSE_CMD $COMPOSE_ARGS --env-file .env pull $TARGET_SERVICES; then
echo "✅ Pull succeeded on attempt $attempt"
PULL_SUCCESS=true
break
else
echo "⚠️ Pull attempt $attempt failed. Retrying in 5s..."
sleep 5
fi
done
if [ "$PULL_SUCCESS" != "true" ]; then
echo "❌ Failed to pull images after 3 attempts."
exit 1
fi
echo "🧹 Clearing Git locks..."
rm -f .git/shallow.lock || true
echo "🧹 Removing stale target containers by container_name..."
# Extract all explicitly defined container_names from compose files and remove them to prevent conflicts
if [ -n "$TARGET_COMPOSE" ]; then
for f in $TARGET_COMPOSE; do
if [ -f "$f" ]; then
grep "container_name:" "$f" | awk '{print $2}' | while read -r cname; do
docker rm -f "$cname" >/dev/null 2>&1 || true
done
fi
done
else
for f in $ALL_COMPOSE_FILES; do
if [ -f "$f" ]; then
grep "container_name:" "$f" | awk '{print $2}' | while read -r cname; do
docker rm -f "$cname" >/dev/null 2>&1 || true
done
fi
done
fi
echo "🆙 Starting services..."
echo "🔍 Debug: Current docker containers:"
docker ps -a
if [ -n "$TARGET_SERVICES" ]; then
$COMPOSE_CMD $COMPOSE_ARGS --env-file .env up -d $TARGET_SERVICES
else
$COMPOSE_CMD $COMPOSE_ARGS --env-file .env up -d --remove-orphans
fi
# ── Traefik reload ──
if [ "${RELOAD_TRAEFIK:-false}" = "true" ]; then
echo "🔄 Traefik dynamic config changed — reloading Traefik..."
# Traefik watches the dynamic config dir (providers.file.watch=true),
# but send SIGHUP as insurance
docker kill --signal HUP traefik 2>/dev/null || docker exec traefik kill -HUP 1 2>/dev/null || true
echo "✅ Traefik reload signal sent"
fi
EOF
-338
View File
@@ -1,338 +0,0 @@
name: Build and Push Docker Images
on:
push:
branches:
- main
paths:
- 'apps/scraper/**'
- 'apps/hub/**'
- 'apps/tools/**'
- 'apps/llm-api/**'
- '.github/workflows/docker-build-push.yml'
- 'infra/**'
- '!infra/compose/**'
repository_dispatch:
types: [submodule-updated]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
env:
REGISTRY: ghcr.io
IMAGE_NAME_PREFIX: asepharyana/asepharyana-hub
jobs:
# ──────────────────────────────────────────────
# Phase 1: Detect which services have changed
# ──────────────────────────────────────────────
changes:
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
scraper-api: ${{ steps.filter.outputs['scraper-api'] == 'true' || steps.dispatch.outputs['scraper-api'] == 'true' || github.event_name == 'workflow_dispatch' }}
hub: ${{ steps.filter.outputs['hub'] == 'true' || steps.dispatch.outputs['hub'] == 'true' || github.event_name == 'workflow_dispatch' }}
tools: ${{ steps.filter.outputs['tools'] == 'true' || steps.dispatch.outputs['tools'] == 'true' || github.event_name == 'workflow_dispatch' }}
llm-api: ${{ steps.filter.outputs['llm-api'] == 'true' || steps.dispatch.outputs['llm-api'] == 'true' || github.event_name == 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v7
with:
submodules: false
fetch-depth: 2
- name: Detect changed services
id: filter
if: github.event_name == 'push'
env:
BEFORE: ${{ github.event.before }}
AFTER: ${{ github.sha }}
run: |
set -euo pipefail
if [ -z "${BEFORE:-}" ] || [[ "$BEFORE" =~ ^0+$ ]]; then
CHANGED_FILES=$(git ls-files)
else
git fetch --no-tags --depth=2 origin "$BEFORE" || true
CHANGED_FILES=$(git diff --name-only "$BEFORE" "$AFTER")
fi
changed() {
printf '%s\n' "$CHANGED_FILES" | grep -Eq "$1" && echo true || echo false
}
echo "scraper-api=$(changed '^(apps/scraper(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/scraper\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
echo "hub=$(changed '^(apps/hub(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/hub\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
echo "tools=$(changed '^(apps/tools(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/tools\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
echo "llm-api=$(changed '^(apps/llm-api(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/llm-api\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
- name: Parse repository_dispatch payload
id: dispatch
if: github.event_name == 'repository_dispatch'
env:
SERVICE: ${{ github.event.client_payload.service }}
SHA: ${{ github.event.client_payload.sha }}
run: |
set -euo pipefail
if [ -z "${SERVICE:-}" ]; then
echo "::error::repository_dispatch payload missing service"
exit 1
fi
if [ -z "${SHA:-}" ]; then
echo "::error::repository_dispatch payload missing sha"
exit 1
fi
case "$SERVICE" in
scraper-api|hub|tools|llm-api) ;;
*)
exit 1
;;
esac
if ! [[ "$SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then echo "::error::Invalid sha '$SHA'. Expected 40 hex characters"; fi
SERVICES=(scraper-api hub tools llm-api)
for svc in "${SERVICES[@]}"; do
if [ "$SERVICE" = "$svc" ]; then
echo "${svc}=true" >> "$GITHUB_OUTPUT"
else
echo "${svc}=false" >> "$GITHUB_OUTPUT"
fi
done
- name: Set matrix
id: set-matrix
run: |
SERVICES=()
add_service() {
SERVICES+=("{\"id\":\"$1\",\"target\":\"$2\",\"path\":\"$3\"}")
}
if [ "${{ steps.filter.outputs['scraper-api'] == 'true' || steps.dispatch.outputs['scraper-api'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "scraper-api" "docker-scraper" "apps/scraper"; fi
if [ "${{ steps.filter.outputs['hub'] == 'true' || steps.dispatch.outputs['hub'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "hub" "docker-hub" "apps/hub"; fi
if [ "${{ steps.filter.outputs['tools'] == 'true' || steps.dispatch.outputs['tools'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "tools" "docker-tools" "apps/tools"; fi
if [ "${{ steps.filter.outputs['llm-api'] == 'true' || steps.dispatch.outputs['llm-api'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "llm-api" "docker-llm-api" "apps/llm-api"; fi
JSON_ARRAY="[$(IFS=,; echo "${SERVICES[*]}")]"
echo "matrix=$JSON_ARRAY" >> $GITHUB_OUTPUT
wait-submodule-ref:
needs: [changes]
if: github.event_name == 'repository_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Wait for submodule ref
env:
SERVICE: ${{ github.event.client_payload.service }}
SHA: ${{ github.event.client_payload.sha }}
run: |
set -euo pipefail
case "$SERVICE" in
"scraper-api") REPO="https://github.com/asepharyana/asepharyana-hub-scraper.git" ;;
"hub") REPO="https://github.com/asepharyana/asepharyana-hub-hub.git" ;;
"tools") echo "tools is built from monorepo, no submodule wait needed"; exit 0 ;;
"llm-api") REPO="https://github.com/asepharyana/asepharyana-hub-llm-api.git" ;;
*)
echo "::error::Unsupported service '$SERVICE'"
exit 1
;;
esac
echo "Waiting for $SERVICE commit $SHA in $REPO"
TMPDIR=$(mktemp -d)
git init "$TMPDIR/probe" >/dev/null
git -C "$TMPDIR/probe" remote add origin "$REPO"
for attempt in {1..30}; do
if git -C "$TMPDIR/probe" fetch --depth=1 origin "$SHA" >/dev/null 2>&1; then
echo "Submodule commit $SHA is fetchable for $SERVICE"
rm -rf "$TMPDIR"
exit 0
fi
echo "Attempt $attempt/30: $SHA not fetchable yet; waiting 10s"
sleep 10
done
rm -rf "$TMPDIR"
echo "::error::Submodule commit $SHA for $SERVICE was not fetchable after 300s"
exit 1
# ─────────────────────────────────────────────────
# Phase 2: Build and Push Images (Matrix)
# ─────────────────────────────────────────────────
build:
needs: [changes, wait-submodule-ref]
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.changes.outputs.matrix) }}
if: |
always() &&
needs.changes.result == 'success' &&
(needs.wait-submodule-ref.result == 'success' || needs.wait-submodule-ref.result == 'skipped') &&
needs.changes.outputs.matrix != '[]'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v7
with:
submodules: false
- name: Sync submodule locally
env:
EVENT_NAME: ${{ github.event_name }}
DISPATCH_SHA: ${{ github.event.client_payload.sha }}
SUBMODULE_PATH: ${{ matrix.path }}
run: |
set -euo pipefail
git submodule update --init --recursive "$SUBMODULE_PATH"
if [ "$EVENT_NAME" = "repository_dispatch" ] && [ -n "${DISPATCH_SHA:-}" ]; then
cd "$SUBMODULE_PATH"
git fetch origin "$DISPATCH_SHA"
git checkout "$DISPATCH_SHA"
cd "${GITHUB_WORKSPACE}"
fi
- uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Docker metadata
id: meta
run: |
SVC_NAME="${{ matrix.id }}"
SHORT=${GITHUB_SHA:0:7}
echo "image=${REGISTRY}/${IMAGE_NAME_PREFIX}/${SVC_NAME}" >> $GITHUB_OUTPUT
echo "tags=${REGISTRY}/${IMAGE_NAME_PREFIX}/${SVC_NAME}:sha-${SHORT}" >> $GITHUB_OUTPUT
echo "cache-registry=${REGISTRY}/${IMAGE_NAME_PREFIX}/${SVC_NAME}:buildcache" >> $GITHUB_OUTPUT
case "$SVC_NAME" in
"scraper-api") echo "dockerfile=infra/docker/scraper.Dockerfile" >> $GITHUB_OUTPUT ;;
"hub") echo "dockerfile=infra/docker/hub.Dockerfile" >> $GITHUB_OUTPUT ;;
"tools") echo "dockerfile=infra/docker/tools.Dockerfile" >> $GITHUB_OUTPUT ;;
"llm-api") echo "dockerfile=infra/docker/llm-api.Dockerfile" >> $GITHUB_OUTPUT ;;
esac
- name: Build and Push Docker image
uses: docker/build-push-action@v7
with:
context: .
file: ${{ steps.meta.outputs.dockerfile }}
push: true
tags: ${{ steps.meta.outputs.tags }}
build-args: |
COMMIT_COUNT=${{ env.NR_COMMIT_COUNT || github.run_number }}
COMMIT_SHA=${{ env.NR_COMMIT_SHA || github.sha }}
cache-from: type=registry,ref=${{ steps.meta.outputs['cache-registry'] }}
cache-to: type=registry,ref=${{ steps.meta.outputs['cache-registry'] }},mode=max
# ──────────────────────────────────────────────
# Phase 3: Update Manifests and Submodule Refs
# ──────────────────────────────────────────────
update-manifest:
needs: [changes, wait-submodule-ref, build]
if: |
always() &&
needs.changes.result == 'success' &&
(needs.wait-submodule-ref.result == 'success' || needs.wait-submodule-ref.result == 'skipped') &&
(needs.build.result == 'success' || needs.build.result == 'skipped')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
submodules: false
token: ${{ secrets.GITHUB_TOKEN }}
ref: main
- name: Update tags and submodules
run: |
SHORT_SHA=${GITHUB_SHA:0:7}
TAG="sha-$SHORT_SHA"
CHANGED=false
declare -A SERVICES
SERVICES["scraper-api"]="scraper.yml"
SERVICES["hub"]="hub.yml"
SERVICES["tools"]="tools.yml"
SERVICES["llm-api"]="llm-api.yml"
declare -A PATHS
PATHS["scraper-api"]="apps/scraper"
PATHS["hub"]="apps/hub"
PATHS["tools"]="apps/tools"
PATHS["llm-api"]="apps/llm-api"
# Use git config for possible commits
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
for id in "${!SERVICES[@]}"; do
SHOULD_HAVE_RUN=false
if [ "${{ needs.changes.outputs['scraper-api'] }}" == "true" ] && [ "$id" == "scraper-api" ]; then SHOULD_HAVE_RUN=true; fi
if [ "${{ needs.changes.outputs['hub'] }}" == "true" ] && [ "$id" == "hub" ]; then SHOULD_HAVE_RUN=true; fi
if [ "${{ needs.changes.outputs['tools'] }}" == "true" ] && [ "$id" == "tools" ]; then SHOULD_HAVE_RUN=true; fi
if [ "${{ needs.changes.outputs['llm-api'] }}" == "true" ] && [ "$id" == "llm-api" ]; then SHOULD_HAVE_RUN=true; fi
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then SHOULD_HAVE_RUN=true; fi
if [ "$SHOULD_HAVE_RUN" == "true" ]; then
COMPOSE_FILE="infra/compose/${SERVICES[$id]}"
if [ -f "$COMPOSE_FILE" ]; then
echo "Updating $COMPOSE_FILE to $TAG"
sed -i "s|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_PREFIX }}/$id:.*|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_PREFIX }}/$id:$TAG|g" "$COMPOSE_FILE"
git add "$COMPOSE_FILE"
CHANGED=true
fi
# If it's a repository_dispatch for this specific service, update its submodule pointer
if [ "${{ github.event_name }}" == "repository_dispatch" ] && [ "${{ github.event.client_payload.service }}" == "$id" ]; then
SHA_DISPATCH="${{ github.event.client_payload.sha }}"
SUB_PATH="${PATHS[$id]}"
if [ -n "$SHA_DISPATCH" ]; then
echo "Updating submodule $SUB_PATH to $SHA_DISPATCH"
git submodule update --init "$SUB_PATH"
git -C "$SUB_PATH" fetch origin "$SHA_DISPATCH"
git -C "$SUB_PATH" checkout "$SHA_DISPATCH"
git add "$SUB_PATH"
CHANGED=true
fi
fi
fi
done
if [ "$CHANGED" == "true" ]; then
git commit -m "chore: update manifests and submodules [skip ci]"
for attempt in {1..3}; do
if git pull --rebase origin main && git push origin main; then
exit 0
fi
echo "Manifest push attempt $attempt/3 failed; retrying"
git rebase --abort || true
git pull --rebase origin main || true
sleep 5
done
echo "::error::Failed to push manifest update after 3 attempts"
exit 1
else
echo "No changes detected."
fi
@@ -12,7 +12,7 @@ jobs:
id-token: write
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: DeterminateSystems/determinate-nix-action@main
- uses: DeterminateSystems/flakehub-push@main
with:
+25 -6
View File
@@ -1,14 +1,11 @@
name: Nix Build & Deploy — All Services
on:
# No `paths` filter: GitHub's path filters do not match submodule gitlink
# changes, so a submodule pointer update (e.g. from update-submodule.yml)
# would never trigger this deploy. Run on every push to main instead.
push:
branches: [main]
paths:
- 'apps/**'
- 'infra/**'
- 'flake.nix'
- 'flake.lock'
- '.github/workflows/nix-build.yml'
workflow_dispatch:
concurrency:
@@ -17,6 +14,7 @@ concurrency:
permissions:
contents: read
id-token: write
env:
VPS_HOST: ${{ secrets.VPS_HOST }}
@@ -47,6 +45,8 @@ jobs:
- name: Cache Nix
uses: DeterminateSystems/magic-nix-cache-action@v14
with:
use-flakehub: false
- name: Build ${{ matrix.service }}
id: build
@@ -82,3 +82,22 @@ jobs:
ssh "$VPS_USER@$VPS_HOST" "sudo systemctl restart ${{ matrix.service }}" || echo " ⚠️ restart failed (may not be enabled yet)"
echo "✅ ${{ matrix.service }} deployed"
cleanup:
# Bersihkan sampah Nix di VPS SETELAH deploy: 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)"
+1
View File
@@ -11,6 +11,7 @@ node_modules
.turbo/
# IDEs and editors
/.idea
.serena/
.project
.classpath
.c9/
+3 -2
View File
@@ -61,10 +61,10 @@
# ── Fetch submodule source ──
submoduleSrc = name: builtins.fetchGit {
url = submoduleRepos.${name};
rev = if name == "hub" then "f35edb00d72675e7f41bdf8f3b54a17e1a241d1a"
rev = if name == "hub" then "a90d0c43336a5f000b5856003d2293c420e7d595"
else if name == "scraper" then "62aa5b0e52859afe3ba9de1c7b11cfe2dacf6c2c"
else if name == "tools" then "3956b90c3ce39ffa7ffba8084937f20e11364d6b"
else if name == "llm-api" then "67861f384bd4f64d5236a9608b33c965fbebbc7f"
else if name == "llm-api" then "5f7ead5503082a71d41a36fd1727325c784e4b79"
else "HEAD";
submodules = true;
};
@@ -88,6 +88,7 @@
cp -r .next $out/share/hub/
cp -r public $out/share/hub/ 2>/dev/null || true
cp package.json $out/share/hub/
cp next.config.{ts,mjs,js} $out/share/hub/ 2>/dev/null || true
cp -r node_modules $out/share/hub/
cat > $out/bin/hub << WRAPPER
#!${pkgs.runtimeShell}
+37 -18
View File
@@ -40,6 +40,14 @@ asepharyana.my.id, www.asepharyana.my.id, asepharyana.web.id, www.asepharyana.we
}
dashboard.asepharyana.my.id {
import proxy 4013
}
imphnen.asepharyana.my.id {
import proxy 4009
}
scraper.asepharyana.my.id, api.asepharyana.my.id, scraper.asepharyana.web.id, api.asepharyana.web.id {
import proxy 4091
}
@@ -53,7 +61,23 @@ tools.asepharyana.my.id, tools.asepharyana.web.id {
9router.asepharyana.my.id {
# LLM streaming: 9router combo models punya TTFT sampe 30-40s (deepseek,
}
# fallback chain). Default (proxy) response_header_timeout 30s / read 60s
# bikin false-positive 504 walau 9router masih ngolah. Longgarkan khusus
# biar health-check & request PR-Agent real gak kena timeout transient.
encode zstd gzip
header {
-Server
X-Content-Type-Options "nosniff"
}
reverse_proxy 127.0.0.1:4014 {
transport http {
dial_timeout 3s
response_header_timeout 120s
read_timeout 300s
write_timeout 300s
}
}
}
pr-agent.asepharyana.my.id {
import proxy 4002
@@ -80,26 +104,21 @@ ml-zeavisedu.asepharyana.my.id {
}
upload.asepharyana.my.id, upload.asepharyana.web.id {
import proxy 4013
}
imphnen.asepharyana.my.id {
import proxy 4009
}
upload.asepharyana.my.id, upload.asepharyana.web.id {
# Upload/download besar: JANGAN kompres, JANGAN limit body, flush instan (no buffering)
reverse_proxy 127.0.0.1:4000 {
transport http {
keepalive 120s
keepalive_interval 30s
max_conns_per_host 100
dial_timeout 3s
}
flush_interval -1
}
header {
-Server
X-Content-Type-Options "nosniff"
}
request_body {
max_size 0
}
reverse_proxy 127.0.0.1:4000 {
transport http {
dial_timeout 3s
read_timeout 300s
write_timeout 300s
}
flush_interval -1
}
}
}
+26 -1
View File
@@ -2,10 +2,30 @@
# ============================================================
# firewall.sh — deny-by-default firewall untuk orangevps
# Public: 22 (SSH), 80/443 (Caddy), 4013 (hermes dashboard)
# 25565 (Minecraft) — WHITELIST TCPShield proxy only
# Tailscale CGNAT 100.64/10: semua port (imrnes & node lain)
# Localhost: semua
# Sisanya: DROP + log
# ============================================================
# TCPShield proxy ranges (https://tcpshield.com/v4/ + /v4-cf/)
# Update saat TCPShield publish range baru.
TCPSHIELD_V4=(
198.178.119.0/24
104.234.6.0/24
)
TCPSHIELD_V4_CF=(
89.222.122.36/31
152.233.22.8/31
89.222.108.246/31
84.17.55.186/31
51.79.45.52/31
5.135.84.92/30
51.75.35.44/30
51.161.27.110/31
152.233.30.16/31
152.233.30.232/31
203.205.31.160/31
)
set -e
### IPv4 ###
@@ -33,6 +53,10 @@ iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Public: hermes dashboard (auth-protected)
iptables -A INPUT -p tcp --dport 4013 -j ACCEPT
# Public: Minecraft (FTB sky) — hanya dari proxy TCPShield
for cidr in "${TCPSHIELD_V4[@]}" "${TCPSHIELD_V4_CF[@]}"; do
iptables -A INPUT -s "$cidr" -p tcp --dport 25565 -j ACCEPT
done
# ICMP (ping, PMTU)
iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 5/sec --limit-burst 10 -j ACCEPT
@@ -66,9 +90,10 @@ ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT
ip6tables -A INPUT -p tcp --dport 80 -j ACCEPT
ip6tables -A INPUT -p tcp --dport 443 -j ACCEPT
ip6tables -A INPUT -p tcp --dport 4013 -j ACCEPT
# Minecraft 25565: TCPShield IPv4 only — tidak ada range IPv6 publik
ip6tables -A INPUT -p icmpv6 -j ACCEPT
ip6tables -A INPUT -m limit --limit 5/min --limit-burst 10 -j LOG --log-prefix "FW6-DROP " --log-level 4
ip6tables -A INPUT -j DROP
echo "Firewall applied:"
iptables -L INPUT -n --line-numbers | head -20
iptables -L INPUT -n --line-numbers | head -24
+4 -1
View File
@@ -11,4 +11,7 @@
labels:
service: gmw-backend
__metrics_path__: /api/metrics
# llm-api tidak expose /metrics (404) — dihapus dari target.
# llm-api expose /metrics sejak 2026-08-03 (feat metrics):
- targets: ['127.0.0.1:4010']
labels:
service: llm-api