Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21743cedfa | ||
|
|
63c3b87f39 | ||
|
|
c5c0a961b0 | ||
|
|
c25c645818 | ||
|
|
9e6078af9b | ||
|
|
bda852f287 | ||
|
|
754bc2876f | ||
|
|
b37daa52b0 | ||
|
|
56cee21e66 | ||
|
|
6ac562a869 | ||
|
|
4c6c368764 | ||
|
|
0194a865b7 | ||
|
|
1f490cf9c2 | ||
|
|
851e82d625 | ||
|
|
8fe5a478b5 | ||
|
|
a2e65e3980 | ||
|
|
1bbba46b9a | ||
|
|
80ea5f0b2e | ||
|
|
6cc583d3b9 | ||
|
|
912fd6864f | ||
|
|
eba29a2fa1 | ||
|
|
76df15616e | ||
|
|
07a81df64d | ||
|
|
bbbb1d2cee | ||
|
|
f9d8445443 | ||
|
|
4f3442f97b | ||
|
|
93cf1ffb9b | ||
|
|
499721c5e3 | ||
|
|
09c5311568 | ||
|
|
e74a303b9d | ||
|
|
0bcd6582f4 | ||
|
|
a38a99b6f4 | ||
|
|
53cc225bff | ||
|
|
a20b0be8da | ||
|
|
76ccf5488b | ||
|
|
29271ccc05 | ||
|
|
6a1d199443 | ||
|
|
eb19385ab4 | ||
|
|
a1c0ef25ef | ||
|
|
609da6b6bc | ||
|
|
402499d109 | ||
|
|
1389d5faa3 | ||
|
|
59e6c5c6a1 | ||
|
|
4f8a975337 | ||
|
|
c251867f05 | ||
|
|
b06cddca4a | ||
|
|
19aface599 | ||
|
|
528a109d53 | ||
|
|
97b635e8c6 | ||
|
|
99e438d9c5 | ||
|
|
8e850f05c4 | ||
|
|
6c2a4fc9d2 | ||
|
|
3e92f68d78 | ||
|
|
2050129062 | ||
|
|
a6148753ca | ||
|
|
d02989f1a7 | ||
|
|
6e317eb05a | ||
|
|
4e114edb89 | ||
|
|
49123c08b3 | ||
|
|
539b5b8320 | ||
|
|
39df52aab7 | ||
|
|
b326911478 | ||
|
|
8242325ffd | ||
|
|
c075cf282b | ||
|
|
93936efd40 | ||
|
|
5f66cfc12b | ||
|
|
cc8c3f1bf3 | ||
|
|
0c36007da4 | ||
|
|
b7d02647d9 | ||
|
|
ade2798ded | ||
|
|
ef14ca431f | ||
|
|
f681e624b3 | ||
|
|
fdbdcbc1ec | ||
|
|
b7b60e9125 | ||
|
|
722709f6fb | ||
|
|
d20643fbbd | ||
|
|
7da1727603 | ||
|
|
9e9fd229d0 | ||
|
|
40095f75f4 | ||
|
|
3781a8882d | ||
|
|
da2f4a2f2c | ||
|
|
ad22a39a9e | ||
|
|
2f969972a1 | ||
|
|
620f06fbe0 | ||
|
|
e6033c8b12 | ||
|
|
500af023de | ||
|
|
e280d196f9 | ||
|
|
4927338c98 | ||
|
|
54ee903647 | ||
|
|
35cbcdcc92 | ||
|
|
324fe08913 | ||
|
|
e171201b1c | ||
|
|
ec1394bdc5 | ||
|
|
f0787b5f23 | ||
|
|
0df6c35d5a | ||
|
|
5d21848e87 | ||
|
|
b96548bffa | ||
|
|
e19fa76dc3 | ||
|
|
c535ee7858 | ||
|
|
80b5161630 | ||
|
|
1ccc0355ac | ||
|
|
12aed642a9 | ||
|
|
f8e3dccb44 | ||
|
|
1811c6adc2 | ||
|
|
ceeac5c02f | ||
|
|
d0c9f16d5d | ||
|
|
6cbafbfe2e | ||
|
|
d6b9a0806b | ||
|
|
60a1af436f | ||
|
|
2459541677 | ||
|
|
33b999d2b7 | ||
|
|
59f131f951 | ||
|
|
cc8c5088a4 | ||
|
|
d15e8621a9 | ||
|
|
33d5b12ec6 | ||
|
|
b5596e1398 | ||
|
|
46730ec07d | ||
|
|
1ae8a53cd4 | ||
|
|
ff52f8841c | ||
|
|
b747ae5c3b | ||
|
|
64ce812b4e | ||
|
|
9837d9dc50 | ||
|
|
86dda74174 | ||
|
|
d045602f3e | ||
|
|
32049c70ac | ||
|
|
be7348a27d | ||
|
|
43f0df4493 | ||
|
|
f0c7534152 | ||
|
|
a4687c9d5b | ||
|
|
e42e967b7f | ||
|
|
d99bc06df3 | ||
|
|
87df0d7ae2 | ||
|
|
518afa5645 | ||
|
|
2913f515f0 | ||
|
|
05e20df680 | ||
|
|
59e2f9d173 | ||
|
|
f78b826e59 | ||
|
|
d75424634d | ||
|
|
cc8cefb33a | ||
|
|
757b48b446 | ||
|
|
20c81b4018 | ||
|
|
ca81a8b7ee | ||
|
|
ac25a51113 | ||
|
|
10aeda4d4b | ||
|
|
5a0737dc74 | ||
|
|
1e4b17afb0 | ||
|
|
22e8a872cd | ||
|
|
18cabefe9c | ||
|
|
f66f25f120 | ||
|
|
865fbf9547 | ||
|
|
1d50980eed | ||
|
|
ef29e0e4ef | ||
|
|
16c0231b9f | ||
|
|
3be2744070 | ||
|
|
3dfde5e872 | ||
|
|
632868356d | ||
|
|
2e3d5c3de6 | ||
|
|
6fcdf967b6 | ||
|
|
ac01422d21 | ||
|
|
95a9921b7a | ||
|
|
fde53f22b6 | ||
|
|
e953c3cb67 | ||
|
|
cbad784bca | ||
|
|
189311015c | ||
|
|
ab3c95dae2 | ||
|
|
05d6b4a055 | ||
|
|
2033d9b2bc | ||
|
|
8fc4a15e0c | ||
|
|
88f6968654 | ||
|
|
08883ba1a6 | ||
|
|
4fbca91c8c | ||
|
|
1cdd849d89 | ||
|
|
de161d95a8 | ||
|
|
c4791b3160 | ||
|
|
e9f3cd458c | ||
|
|
14c9668e85 | ||
|
|
6f1dfea710 | ||
|
|
3c52036f7e | ||
|
|
e10a2e498b | ||
|
|
183659b56c | ||
|
|
38873dd73e | ||
|
|
be2117aa16 | ||
|
|
f92aa8ef30 | ||
|
|
4be55b57bb | ||
|
|
5c79a91a2e | ||
|
|
13e2c38eef | ||
|
|
8f62f955d1 | ||
|
|
d2f592261e | ||
|
|
70f54c8c7a | ||
|
|
a35e19b546 | ||
|
|
c25850b727 | ||
|
|
97b48ddb21 | ||
|
|
ab994ffd54 | ||
|
|
9d0b075b92 | ||
|
|
c5a7ae247a | ||
|
|
cbda54fdee | ||
|
|
836ef6862e | ||
|
|
bd751c0fca | ||
|
|
ad7a0de37b | ||
|
|
05a9179356 | ||
|
|
584a7155e6 | ||
|
|
a41b2d7312 | ||
|
|
c8fcaecbdb | ||
|
|
d8b1457a11 | ||
|
|
67288c8723 | ||
|
|
9c12d135e4 | ||
|
|
68dea35d73 | ||
|
|
447b22d9da | ||
|
|
e38fe75d3c | ||
|
|
ddc4c58243 | ||
|
|
1e10973c86 | ||
|
|
0242fc4b68 | ||
|
|
b7e4203a15 | ||
|
|
7181710aa7 | ||
|
|
478a0f849f | ||
|
|
7fc863d611 | ||
|
|
df7aaedfba | ||
|
|
93d053b123 | ||
|
|
401a26aea9 | ||
|
|
0a4952ce7f | ||
|
|
23ba9c48fa | ||
|
|
7e92351f45 | ||
|
|
8e489b9f41 | ||
|
|
0dbaf66743 | ||
|
|
6091d358e1 | ||
|
|
c17f5f5b32 | ||
|
|
83b825b449 | ||
|
|
48086de66b | ||
|
|
545397eb1f | ||
|
|
40e1a80f9f | ||
|
|
688ac095f6 | ||
|
|
a4852f636a | ||
|
|
69cae1e73e | ||
|
|
bd9cc1f4c7 | ||
|
|
aaaf5de70c | ||
|
|
cea96fcfc2 | ||
|
|
5ff8f9224f | ||
|
|
caf3f1d79b | ||
|
|
4e4ed101c5 | ||
|
|
9db886e68b | ||
|
|
cdaa3c3d57 | ||
|
|
f085375363 | ||
|
|
5999ac159a | ||
|
|
28ba9dd78c | ||
|
|
4cc2ba1630 | ||
|
|
bf39884ea6 | ||
|
|
373cb4fc1a | ||
|
|
f96041a164 | ||
|
|
9a48a49f21 | ||
|
|
4f15cafc02 | ||
|
|
6c24508c77 | ||
|
|
c909c4a9a1 | ||
|
|
aa05820393 | ||
|
|
ffe07ddef0 | ||
|
|
0bc61d8b1c | ||
|
|
afae724f6b | ||
|
|
9f54f90621 | ||
|
|
ee4fd677ea | ||
|
|
1ee99ffc2c | ||
|
|
46314e6d38 | ||
|
|
362e9f4115 | ||
|
|
da268f1671 | ||
|
|
4ef4f615c1 | ||
|
|
5d5a0f48b5 | ||
|
|
dcb93035f3 | ||
|
|
df71487563 | ||
|
|
5025d103e9 | ||
|
|
fd3c7f8c29 | ||
|
|
979a70b279 | ||
|
|
a3c5d747ca | ||
|
|
ce7f630894 | ||
|
|
ff047b44de | ||
|
|
8e128568e7 | ||
|
|
8f22d052f8 | ||
|
|
005fe466ed | ||
|
|
b7180ab30c | ||
|
|
299756d285 | ||
|
|
ad67ed9711 | ||
|
|
2bb1f1e814 | ||
|
|
aa83986b23 | ||
|
|
9b9ce9e59e | ||
|
|
73011d9d14 | ||
|
|
a659f598ee | ||
|
|
1befc9285a | ||
|
|
2f3bc950a6 | ||
|
|
7bf4d69f17 | ||
|
|
e54c959765 | ||
|
|
e825c553d6 | ||
|
|
61cd1a712a | ||
|
|
cc3e891960 | ||
|
|
b960f9915c | ||
|
|
7e563b7a54 | ||
|
|
64b8bb29a3 | ||
|
|
443ef9a8da | ||
|
|
36de54dd85 | ||
|
|
79015974df | ||
|
|
f7f8ee1beb | ||
|
|
a6b822bbfe | ||
|
|
0ba5da78f9 | ||
|
|
26d980fc00 | ||
|
|
1098f97c05 | ||
|
|
3fdf1c670f | ||
|
|
3299759dae | ||
|
|
bc153b7491 | ||
|
|
05f3a65358 | ||
|
|
7b983daa4c | ||
|
|
9a29d86fbd | ||
|
|
ea168b4b20 | ||
|
|
d546a1acf9 | ||
|
|
23269e1ab2 | ||
|
|
8f05d1df3d | ||
|
|
28934aaafc | ||
|
|
75da3af5b8 | ||
|
|
c4ecf8f21b | ||
|
|
f465ca261e | ||
|
|
092c0b363d | ||
|
|
84d6056e5e | ||
|
|
80027ee5d3 | ||
|
|
5d6bdf6ce3 | ||
|
|
cdad9ae720 | ||
|
|
44a56fa0a9 | ||
|
|
8145aff114 | ||
|
|
45ab3c8792 |
@@ -1 +0,0 @@
|
|||||||
1.3.11
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"image": "mcr.microsoft.com/devcontainers/universal:2",
|
|
||||||
"features": {
|
|
||||||
"ghcr.io/devcontainers/features/rust:1": {},
|
|
||||||
"ghcr.io/devcontainers/features/node:2": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
**/.git
|
|
||||||
**/.gitmodules
|
|
||||||
**/node_modules
|
|
||||||
**/dist
|
|
||||||
**/.output
|
|
||||||
**/target
|
|
||||||
**/.svelte-kit
|
|
||||||
**/.next
|
|
||||||
**/.DS_Store
|
|
||||||
**/build
|
|
||||||
!apps/*/scripts/build/
|
|
||||||
!apps/*/src/**/build/
|
|
||||||
**/*.log
|
|
||||||
**/*.pem
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# IDE and temporary files
|
|
||||||
**/.vscode
|
|
||||||
**/.idea
|
|
||||||
**/tmp
|
|
||||||
**/temp
|
|
||||||
**/.cache
|
|
||||||
**/coverage
|
|
||||||
**/.npm
|
|
||||||
**/.bun
|
|
||||||
**/.pnpm-store
|
|
||||||
**/.yarn
|
|
||||||
**/.cargo-ok
|
|
||||||
**/*.swp
|
|
||||||
**/*~
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
name: Dependabot Auto-Merge
|
||||||
|
on: pull_request
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-merge:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.actor == 'dependabot[bot]'
|
||||||
|
steps:
|
||||||
|
- name: Enable auto-merge for Dependabot PR
|
||||||
|
run: gh pr merge --auto --merge "$PR_URL"
|
||||||
|
env:
|
||||||
|
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
+14
-17
@@ -1,21 +1,18 @@
|
|||||||
# To get started with Dependabot version updates, you'll need to specify which
|
|
||||||
# package ecosystems to update and where the package manifests are located.
|
|
||||||
# Please see the documentation for more information:
|
|
||||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
|
||||||
# https://containers.dev/guide/dependabot
|
|
||||||
|
|
||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: 'devcontainers'
|
- package-ecosystem: "npm"
|
||||||
directory: '/'
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
- package-ecosystem: 'github-actions'
|
|
||||||
directory: '/'
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
groups:
|
groups:
|
||||||
github-actions:
|
production:
|
||||||
patterns:
|
dependency-type: "production"
|
||||||
- '*'
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
development:
|
||||||
|
dependency-type: "development"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
|||||||
@@ -1,254 +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@v6
|
|
||||||
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/elysia.yml infra/compose/react.yml infra/compose/rust-auth.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
|
|
||||||
|
|
||||||
# Submodule update removed for faster VPS deployments
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Construct compose arguments
|
|
||||||
docker rm -f imphenbot-app || true
|
|
||||||
|
|
||||||
if [ -n "$TARGET_COMPOSE" ]; then
|
|
||||||
COMPOSE_ARGS=""
|
|
||||||
for f in $TARGET_COMPOSE; do
|
|
||||||
if [ -f "$f" ]; then
|
|
||||||
COMPOSE_ARGS="$COMPOSE_ARGS -f $f"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
UP_FLAGS="-d" # No --remove-orphans for selective updates to avoid killing other services
|
|
||||||
else
|
|
||||||
echo "🚀 Performing full deployment of all services..."
|
|
||||||
COMPOSE_ARGS=""
|
|
||||||
for f in $ALL_COMPOSE_FILES; do
|
|
||||||
COMPOSE_ARGS="$COMPOSE_ARGS -f $f"
|
|
||||||
done
|
|
||||||
UP_FLAGS="-d --remove-orphans"
|
|
||||||
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; 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
|
|
||||||
|
|
||||||
# Cooldown to allow daemon to settle (optional but kept for safety)
|
|
||||||
# echo "⏳ Waiting for Docker daemon to settle..."
|
|
||||||
# sleep 2
|
|
||||||
|
|
||||||
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
|
|
||||||
$COMPOSE_CMD $COMPOSE_ARGS --env-file .env up $UP_FLAGS
|
|
||||||
|
|
||||||
# ── 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
|
|
||||||
|
|
||||||
# - name: Restart coolify-proxy (delay 1 min)
|
|
||||||
# env:
|
|
||||||
# SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
|
||||||
# VPS_HOST: ${{ secrets.VPS_HOST }}
|
|
||||||
# VPS_USER: ${{ secrets.VPS_USER }}
|
|
||||||
# run: |
|
|
||||||
# mkdir -p ~/.ssh
|
|
||||||
# echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
|
||||||
# chmod 600 ~/.ssh/id_rsa
|
|
||||||
# ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts
|
|
||||||
#
|
|
||||||
# echo "⏳ Waiting 60s for containers to settle before restarting proxy..."
|
|
||||||
# sleep 60
|
|
||||||
#
|
|
||||||
# echo "🔄 Restarting coolify-proxy..."
|
|
||||||
# ssh "${VPS_USER}@${VPS_HOST}" "docker restart coolify-proxy"
|
|
||||||
# echo "✅ coolify-proxy restarted."
|
|
||||||
@@ -1,334 +0,0 @@
|
|||||||
name: Build and Push Docker Images
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- 'apps/**'
|
|
||||||
- '.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' }}
|
|
||||||
elysia-api: ${{ steps.filter.outputs['elysia-api'] == 'true' || steps.dispatch.outputs['elysia-api'] == 'true' || github.event_name == 'workflow_dispatch' }}
|
|
||||||
react-web: ${{ steps.filter.outputs['react-web'] == 'true' || steps.dispatch.outputs['react-web'] == 'true' || github.event_name == 'workflow_dispatch' }}
|
|
||||||
rust-auth: ${{ steps.filter.outputs['rust-auth'] == 'true' || steps.dispatch.outputs['rust-auth'] == 'true' || github.event_name == 'workflow_dispatch' }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
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 "elysia-api=$(changed '^(apps/elysia(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/elysia\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "react-web=$(changed '^(apps/react(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/react\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "rust-auth=$(changed '^(apps/rust-auth(/|$)|infra/docker/rust\.Dockerfile$|\.github/workflows/docker-build-push\.yml$|\.gitmodules$)')" >> "$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|elysia-api|react-web|rust-auth) ;;
|
|
||||||
*)
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if ! [[ "$SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then echo "::error::Invalid sha '$SHA'. Expected 40 hex characters"; exit 1; fi
|
|
||||||
|
|
||||||
SERVICES=(scraper-api elysia-api react-web rust-auth)
|
|
||||||
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['elysia-api'] == 'true' || steps.dispatch.outputs['elysia-api'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "elysia-api" "docker-elysia" "apps/elysia"; fi
|
|
||||||
if [ "${{ steps.filter.outputs['react-web'] == 'true' || steps.dispatch.outputs['react-web'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "react-web" "docker-react" "apps/react"; fi
|
|
||||||
if [ "${{ steps.filter.outputs['rust-auth'] == 'true' || steps.dispatch.outputs['rust-auth'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "rust-auth" "docker-rust-auth" "apps/rust-auth"; 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" ;;
|
|
||||||
"elysia-api") REPO="https://github.com/asepharyana/asepharyana-hub-elysia.git" ;;
|
|
||||||
"react-web") REPO="https://github.com/asepharyana/asepharyana-hub-react.git" ;;
|
|
||||||
"rust-auth") REPO="https://github.com/asepharyana/asepharyana-hub-rust-auth.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@v6
|
|
||||||
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"
|
|
||||||
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}:latest,${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 ;;
|
|
||||||
"elysia-api") echo "dockerfile=infra/docker/elysia.Dockerfile" >> $GITHUB_OUTPUT ;;
|
|
||||||
"react-web") echo "dockerfile=infra/docker/react.Dockerfile" >> $GITHUB_OUTPUT ;;
|
|
||||||
"rust-auth") echo "dockerfile=infra/docker/rust.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@v6
|
|
||||||
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["elysia-api"]="elysia.yml"
|
|
||||||
SERVICES["react-web"]="react.yml"
|
|
||||||
SERVICES["rust-auth"]="rust-auth.yml"
|
|
||||||
|
|
||||||
declare -A PATHS
|
|
||||||
PATHS["scraper-api"]="apps/scraper"
|
|
||||||
PATHS["elysia-api"]="apps/elysia"
|
|
||||||
PATHS["react-web"]="apps/react"
|
|
||||||
PATHS["rust-auth"]="apps/rust-auth"
|
|
||||||
|
|
||||||
# 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['elysia-api'] }}" == "true" ] && [ "$id" == "elysia-api" ]; then SHOULD_HAVE_RUN=true; fi
|
|
||||||
if [ "${{ needs.changes.outputs['react-web'] }}" == "true" ] && [ "$id" == "react-web" ]; then SHOULD_HAVE_RUN=true; fi
|
|
||||||
if [ "${{ needs.changes.outputs['rust-auth'] }}" == "true" ] && [ "$id" == "rust-auth" ]; 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
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
name: Deploy Infra Config
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'infra/caddy/**'
|
||||||
|
- 'infra/firewall/**'
|
||||||
|
- 'infra/systemd/**'
|
||||||
|
- 'infra/prometheus/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: infra-deploy
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
VPS_HOST: ${{ secrets.VPS_HOST }}
|
||||||
|
VPS_USER: ${{ secrets.VPS_USER }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-infra:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Validate Caddyfile syntax
|
||||||
|
run: |
|
||||||
|
curl -fsSL "https://caddyserver.com/api/download?os=linux&arch=amd64" -o /tmp/caddy
|
||||||
|
chmod +x /tmp/caddy
|
||||||
|
/tmp/caddy validate --config infra/caddy/Caddyfile.prod --adapter caddyfile 2>&1 | tail -5
|
||||||
|
echo "✅ Caddyfile valid"
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- name: Sync Caddyfile to VPS
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
ssh "$VPS_USER@$VPS_HOST" "sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak-previous"
|
||||||
|
scp -q infra/caddy/Caddyfile.prod "$VPS_USER@$VPS_HOST":/tmp/Caddyfile.new
|
||||||
|
ssh "$VPS_USER@$VPS_HOST" "sudo cp /tmp/Caddyfile.new /etc/caddy/Caddyfile && sudo rm -f /tmp/Caddyfile.new"
|
||||||
|
echo "✅ Caddyfile synced"
|
||||||
|
|
||||||
|
- name: Sync systemd drop-ins to VPS
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
if [ -d infra/systemd ]; then
|
||||||
|
for f in infra/systemd/*; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
base=$(basename "$f")
|
||||||
|
echo " syncing $base"
|
||||||
|
scp -q "$f" "$VPS_USER@$VPS_HOST":/tmp/"$base"
|
||||||
|
ssh "$VPS_USER@$VPS_HOST" "sudo mkdir -p /etc/systemd/system && sudo cp /tmp/$base /etc/systemd/system/$base && sudo rm -f /tmp/$base"
|
||||||
|
done
|
||||||
|
ssh "$VPS_USER@$VPS_HOST" "sudo systemctl daemon-reload"
|
||||||
|
echo "✅ systemd drop-ins synced"
|
||||||
|
else
|
||||||
|
echo "no infra/systemd/ files"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Sync prometheus targets to VPS
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
if [ -f infra/prometheus/targets.yml ]; then
|
||||||
|
scp -q infra/prometheus/targets.yml "$VPS_USER@$VPS_HOST":/tmp/targets.yml
|
||||||
|
ssh "$VPS_USER@$VPS_HOST" "sudo cp /tmp/targets.yml /etc/prometheus/targets.yml 2>/dev/null && sudo rm -f /tmp/targets.yml && sudo systemctl reload prometheus 2>/dev/null || true"
|
||||||
|
echo "✅ prometheus targets synced"
|
||||||
|
else
|
||||||
|
echo "no infra/prometheus/targets.yml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Reload Caddy
|
||||||
|
run: |
|
||||||
|
ssh "$VPS_USER@$VPS_HOST" "sudo systemctl reload caddy || sudo systemctl restart caddy"
|
||||||
|
sleep 3
|
||||||
|
ssh "$VPS_USER@$VPS_HOST" "systemctl is-active caddy"
|
||||||
|
|
||||||
|
- name: Verify routes
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
for u in hub.asepharyana.my.id scraper.asepharyana.my.id tools.asepharyana.my.id wiki.asepharyana.my.id upload.asepharyana.my.id ai.asepharyana.my.id; do
|
||||||
|
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "https://$u/" || true)
|
||||||
|
echo "$u -> $code"
|
||||||
|
case "$code" in
|
||||||
|
000|502|503|504) echo "::error::$u bad status $code"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
echo "✅ All routes reachable"
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
name: Lint
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- 'apps/*/src/**/*.ts'
|
|
||||||
- 'apps/*/src/**/*.tsx'
|
|
||||||
- 'biome.json'
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- 'apps/*/src/**/*.ts'
|
|
||||||
- 'apps/*/src/**/*.tsx'
|
|
||||||
- 'biome.json'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
biome:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
- run: npm install -g @biomejs/biome
|
|
||||||
- run: biome ci . --no-errors-on-unmatched
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
name: Security
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
schedule:
|
|
||||||
- cron: '0 6 * * 1' # Every Monday
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
codeql:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 30
|
|
||||||
permissions:
|
|
||||||
security-events: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: github/codeql-action/init@v3
|
|
||||||
with:
|
|
||||||
languages: javascript-typescript, rust
|
|
||||||
- uses: github/codeql-action/analyze@v3
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
name: TypeCheck
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- 'apps/react/src/**/*.ts'
|
|
||||||
- 'apps/react/src/**/*.tsx'
|
|
||||||
- 'apps/elysia/src/**/*.ts'
|
|
||||||
- 'apps/elysia/tsconfig.json'
|
|
||||||
- 'tsconfig.base.json'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
typecheck:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: oven/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: latest
|
|
||||||
|
|
||||||
- name: TypeCheck apps/react
|
|
||||||
working-directory: apps/react
|
|
||||||
run: |
|
|
||||||
bun install
|
|
||||||
npx tsc --noEmit
|
|
||||||
|
|
||||||
- name: TypeCheck apps/elysia
|
|
||||||
working-directory: apps/elysia
|
|
||||||
run: |
|
|
||||||
bun install
|
|
||||||
bun run typecheck
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
name: Update Submodule Pointer
|
|
||||||
on:
|
|
||||||
repository_dispatch:
|
|
||||||
types: [submodule-updated]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Validate payload
|
|
||||||
env:
|
|
||||||
SERVICE: ${{ github.event.client_payload.service }}
|
|
||||||
SHA: ${{ github.event.client_payload.sha }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if [ -z "${SERVICE:-}" ]; then
|
|
||||||
echo "::error::Missing service in payload"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${SHA:-}" ]; then
|
|
||||||
echo "::error::Missing sha in payload"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! [[ "$SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
|
|
||||||
echo "::error::Invalid sha '$SHA'. Expected 40 hex characters."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$SERVICE" in
|
|
||||||
scraper-api|elysia-api|react-web|rust-auth) ;;
|
|
||||||
*)
|
|
||||||
echo "::error::Unsupported service '$SERVICE'"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "Payload validated: $SERVICE → $SHA"
|
|
||||||
|
|
||||||
- name: Update submodule pointer
|
|
||||||
env:
|
|
||||||
SERVICE: ${{ github.event.client_payload.service }}
|
|
||||||
SHA: ${{ github.event.client_payload.sha }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
echo "Updating ${SERVICE} to ${SHA}"
|
|
||||||
git submodule update --init "apps/${SERVICE}"
|
|
||||||
cd "apps/${SERVICE}"
|
|
||||||
# full fetch so we get tree objects for the target SHA
|
|
||||||
git fetch --depth=1 origin master 2>/dev/null || git fetch --depth=1 origin main
|
|
||||||
git checkout "${SHA}"
|
|
||||||
cd "${GITHUB_WORKSPACE}"
|
|
||||||
git add "apps/${SERVICE}"
|
|
||||||
git diff --cached --quiet && exit 0
|
|
||||||
|
|
||||||
git config user.name "monrepo-bot"
|
|
||||||
git config user.email "monrepo-bot@users.noreply.github.com"
|
|
||||||
git commit -m "chore: update ${SERVICE} to ${SHA:0:12}"
|
|
||||||
|
|
||||||
for attempt in {1..3}; do
|
|
||||||
if git pull --rebase origin main && git push origin main; then
|
|
||||||
echo "✅ Push succeeded on attempt $attempt"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "⚠️ Push attempt $attempt/3 failed; retrying..."
|
|
||||||
git rebase --abort 2>/dev/null || true
|
|
||||||
sleep 3
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "::error::Failed to push submodule update after 3 attempts"
|
|
||||||
exit 1
|
|
||||||
+6
-3
@@ -1,4 +1,3 @@
|
|||||||
apps/gmw/
|
|
||||||
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||||
|
|
||||||
# compiled output
|
# compiled output
|
||||||
@@ -12,6 +11,7 @@ node_modules
|
|||||||
.turbo/
|
.turbo/
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
/.idea
|
/.idea
|
||||||
|
.serena/
|
||||||
.project
|
.project
|
||||||
.classpath
|
.classpath
|
||||||
.c9/
|
.c9/
|
||||||
@@ -35,16 +35,18 @@ npm-debug.log
|
|||||||
yarn-error.log
|
yarn-error.log
|
||||||
testem.log
|
testem.log
|
||||||
/typings
|
/typings
|
||||||
|
.playwright-mcp/
|
||||||
|
|
||||||
# System Files
|
# System Files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
.claude
|
.claude/*
|
||||||
|
!.claude/skills/
|
||||||
|
!.claude/settings.json
|
||||||
# Next.js
|
# Next.js
|
||||||
.next
|
.next
|
||||||
out
|
out
|
||||||
**/.codegraph/**
|
**/.codegraph/**
|
||||||
**/.claude/**
|
|
||||||
test-output
|
test-output
|
||||||
**/**.env
|
**/**.env
|
||||||
**/**.env.**
|
**/**.env.**
|
||||||
@@ -62,3 +64,4 @@ docs/todo.md
|
|||||||
**/vendor/
|
**/vendor/
|
||||||
|
|
||||||
.codegraph/
|
.codegraph/
|
||||||
|
result
|
||||||
|
|||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
[submodule "apps/elysia"]
|
|
||||||
path = apps/elysia
|
|
||||||
url = https://github.com/asepharyana/asepharyana-hub-elysia.git
|
|
||||||
[submodule "apps/react"]
|
|
||||||
path = apps/react
|
|
||||||
url = https://github.com/asepharyana/asepharyana-hub-react.git
|
|
||||||
[submodule "apps/scraper"]
|
|
||||||
path = apps/scraper
|
|
||||||
url = https://github.com/asepharyana/asepharyana-hub-scraper.git
|
|
||||||
[submodule "apps/rust-auth"]
|
|
||||||
path = apps/rust-auth
|
|
||||||
url = https://github.com/asepharyana/asepharyana-hub-rust-auth.git
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"next-devtools": {
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "next-devtools-mcp@latest"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# Agent Protocol v10: Strategic Execution and Diagnostic Directives
|
|
||||||
|
|
||||||
1. **Mandatory Executability.**
|
|
||||||
- **Directive:** All generated outputs must be syntactically correct and directly executable by the target system's interpreter, compiler, or shell.
|
|
||||||
- **Constraint:** Placeholders and pseudo-code are forbidden. Every output must be a complete, functional artifact.
|
|
||||||
|
|
||||||
2. **Data and State Integrity.**
|
|
||||||
- **Directive:** All generated code must strictly adhere to declared data structures, schemas, and the target language's type system.
|
|
||||||
- **Constraint:** Any operation producing a type mismatch, schema violation, or logical inconsistency is an invalid operation and must be discarded.
|
|
||||||
|
|
||||||
3. **Atomic and Consistent State Modification.**
|
|
||||||
- **Directive:** Modification of a shared resource must be performed as an atomic operation or within an ACID-compliant transaction.
|
|
||||||
- **Constraint:** Operations that can lead to race conditions or inconsistent state are prohibited. Immutability is the required default.
|
|
||||||
|
|
||||||
4. **Zero-Trust Security (Inviolable Safety Constraint).**
|
|
||||||
- **Directive:** Secrets must not be stored as literal values in source code. They must be loaded at runtime from a secure external source.
|
|
||||||
- **Constraint:** Generated access policies must adhere to the Principle of Least Privilege.
|
|
||||||
|
|
||||||
5. **Supply Chain Security (Inviolable Safety Constraint).**
|
|
||||||
- **Directive:** All external dependencies must be sourced from trusted repositories and defined in a lockfile for deterministic resolution.
|
|
||||||
- **Constraint:** The dependency graph must be scanned for known CVEs. Dependencies with critical vulnerabilities are prohibited.
|
|
||||||
|
|
||||||
6. **Deterministic and Reproducible Builds.**
|
|
||||||
- **Directive:** From a given source commit, the build process must produce a byte-for-byte identical artifact in every execution.
|
|
||||||
- **Constraint:** All automated tests must be deterministic. A regression test codifying the fixed bug's failure condition must be included with the fix.
|
|
||||||
|
|
||||||
7. **Structured, Traceable Logging.**
|
|
||||||
- **Directive:** All processes must emit structured (JSON) logs for significant events. All log entries for a request must contain the same unique trace ID.
|
|
||||||
- **Constraint:** Error conditions must be explicitly logged with context and propagated. Errors must not be silently suppressed.
|
|
||||||
|
|
||||||
8. **Strict API Contract Enforcement.**
|
|
||||||
- **Directive:** All network communication must strictly conform to its published, versioned API contract.
|
|
||||||
- **Constraint:** Any network call violating the contract must be rejected. Breaking changes require a major version increment (SemVer).
|
|
||||||
|
|
||||||
9. **Distributed System Consensus.**
|
|
||||||
- **Directive:** Changes to shared state across a distributed system are committed only after a formal consensus algorithm confirms quorum.
|
|
||||||
- **Constraint:** Nodes in a minority partition must enter a read-only or unavailable state to prevent a split-brain scenario.
|
|
||||||
|
|
||||||
10. **Execution Planning and Pre-flight Validation (Think Before Acting).**
|
|
||||||
- **Directive:** For any multi-step task, a detailed execution plan (sequence of commands and file modifications) must be formulated before any state-modifying action is taken.
|
|
||||||
- **Constraint:** Before executing a command, the agent must first use a validation or dry-run flag (e.g., `--dry-run`, `--check`) if available. The operation may only proceed if the pre-flight check passes without error.
|
|
||||||
|
|
||||||
11. **Post-Failure Root Cause Analysis (Evaluate Mistakes from Logs).**
|
|
||||||
- **Directive:** Upon command execution failure (non-zero exit code), the current execution plan must be halted, and the agent must enter a diagnostic mode.
|
|
||||||
- **Constraint:** In diagnostic mode, the agent is required to: 1) Capture and parse the complete `stdout` and `stderr` logs. 2) Identify the specific error message or stack trace. 3) Correlate the error with the last command to form a root cause hypothesis. 4) Formulate a new, corrective execution plan based on the analysis.
|
|
||||||
|
|
||||||
12. **Context-Aware File System Operations.**
|
|
||||||
- **Directive:** Before modifying any file, its full content must be read to establish context. All edits must be based on an in-memory understanding of the file's current state.
|
|
||||||
- **Constraint:** Blind file operations, such as stream-based search-and-replace without structural validation, are strictly prohibited.
|
|
||||||
|
|
||||||
13. **Idempotent State Transitions.**
|
|
||||||
- **Directive:** Operations that modify state must be designed to be idempotent wherever the protocol allows.
|
|
||||||
- **Constraint:** Executing the same operation multiple times must result in the same final system state as executing it only once.
|
|
||||||
|
|
||||||
14. **Resource Lifecycle Management.**
|
|
||||||
- **Directive:** All finite system resources (e.g., file handles, network sockets) must be explicitly released after use.
|
|
||||||
- **Constraint:** The agent must generate code that prevents resource leaks, utilizing language-specific constructs like `try-with-resources` or `defer`.
|
|
||||||
|
|
||||||
15. **Configuration as Code (CaC).**
|
|
||||||
- **Directive:** All configuration must be defined and versioned in source-controlled files.
|
|
||||||
- **Constraint:** Manual, out-of-band configuration changes are prohibited. Versioned files are the single source of truth.
|
|
||||||
|
|
||||||
16. **Atomic and Semantic Version Control.**
|
|
||||||
- **Directive:** All code changes must be organized into logically atomic commits representing one complete unit of work.
|
|
||||||
- **Constraint:** Commit messages must adhere to a defined specification (e.g., Conventional Commits).
|
|
||||||
|
|
||||||
17. **User Authority and Command Primacy.**
|
|
||||||
- **Directive:** User-provided instructions and corrections are the definitive source of truth and have the highest operational priority.
|
|
||||||
- **Constraint:** The agent must immediately adapt its process to align with user directives. Rejected solutions must not be proposed again.
|
|
||||||
|
|
||||||
18. **Precedent-Based Improvement.**
|
|
||||||
- **Directive:** User-approved outputs and successful patterns must be recorded and prioritized as precedents for subsequent tasks.
|
|
||||||
- **Constraint:** Performance, security, and code quality must not degrade.
|
|
||||||
|
|
||||||
19. **Optimization by Explicit Consent.**
|
|
||||||
- **Directive:** The agent may identify and propose optimizations with a technical justification and supporting metrics.
|
|
||||||
- **Constraint:** The agent is prohibited from applying any self-initiated optimization without an explicit "approve" command from the user.
|
|
||||||
|
|
||||||
20. **System Hierarchy and Safety Overrides.**
|
|
||||||
- **Directive:** The operational control hierarchy is absolute: 1) **User Command**, 2) **Inviolable Safety Directives (#4, #5)**, 3) **Standard Operational Directives**.
|
|
||||||
- **Constraint:** If a command conflicts with an Inviolable Directive, the agent must halt, report the conflict and risk, and await a revised command.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
22.11.0
|
|
||||||
Vendored
-8
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"nrwl.angular-console",
|
|
||||||
"biomejs.biome",
|
|
||||||
"ms-playwright.playwright",
|
|
||||||
"firsttris.vscode-jest-runner"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Vendored
-23
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[javascriptreact]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[typescriptreact]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[json]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[jsonc]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.organizeImports.biome": "explicit"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-332
@@ -1,332 +0,0 @@
|
|||||||
# Architecture
|
|
||||||
|
|
||||||
## Hub Repository Structure Overview
|
|
||||||
|
|
||||||
```
|
|
||||||
asepharyana-hub/
|
|
||||||
├── apps/ # Application services (Git submodules)
|
|
||||||
│ ├── elysia/ # Realtime API (Bun/Elysia/Drizzle/Redis)
|
|
||||||
│ ├── react/ # Frontend SPA (React/Vite/TanStack)
|
|
||||||
│ ├── rust-auth/ # IAM & auth service (Axum/SeaORM)
|
|
||||||
│ └── scraper/ # Web scraper service
|
|
||||||
├── docs/ # Documentation
|
|
||||||
│ ├── adr/ # Architecture Decision Records
|
|
||||||
│ ├── add-new-app.md # Guide for adding new services
|
|
||||||
│ └── superpowers/ # Project capabilities tracking
|
|
||||||
├── infra/ # Infrastructure as code
|
|
||||||
│ ├── compose/ # Docker Compose files per service
|
|
||||||
│ ├── config/ # Infrastructure configuration
|
|
||||||
│ ├── docker/ # Dockerfiles per service
|
|
||||||
│ └── traefik/ # Traefik reverse proxy config
|
|
||||||
│ └── dynamic/ # Dynamic routing rules (YAML)
|
|
||||||
├── scripts/ # Utility scripts
|
|
||||||
│ ├── git-hooks/ # Git hook scripts
|
|
||||||
│ ├── cleanup-ghcr.sh # GHCR image cleanup
|
|
||||||
│ └── update-deps.sh # Dependency update helper
|
|
||||||
├── .github/workflows/ # CI/CD pipelines
|
|
||||||
├── eslint.config.mjs # Root ESLint config
|
|
||||||
├── package.json # Root formatting/lint helper scripts
|
|
||||||
└── .prettierrc # Prettier formatting rules
|
|
||||||
```
|
|
||||||
|
|
||||||
## Technology Stack
|
|
||||||
|
|
||||||
### Backend Services
|
|
||||||
|
|
||||||
| Service | Language/Runtime | Framework | Database | Key Libraries |
|
|
||||||
| ------------- | ---------------- | ---------- | ------------------------ | ------------------------------------------------------------ |
|
|
||||||
| **rust-auth** | Rust | Axum 0.8 | PostgreSQL (sqlx) | sqlx, jsonwebtoken, argon2, redis, opentelemetry, prometheus |
|
|
||||||
| **elysia** | TypeScript/Bun | Elysia 1.4 | PostgreSQL (Drizzle ORM) | Drizzle ORM, Redis (ioredis), JWT (jose), OTel, Swagger |
|
|
||||||
| **scraper** | _(submodule)_ | — | — | — |
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
|
|
||||||
| Service | Framework | Build Tool | Key Libraries |
|
|
||||||
| --------- | --------- | ---------- | --------------------------------------------------------------------------------------------------- |
|
|
||||||
| **react** | React 19 | Vite 7 | TanStack Router + Query, Three.js/React Three Fiber, Tailwind CSS 4, Zustand, Recharts, tsParticles |
|
|
||||||
|
|
||||||
### Infrastructure
|
|
||||||
|
|
||||||
| Component | Technology | Purpose |
|
|
||||||
| ------------------ | ----------------------- | ---------------------------------------------------------------- |
|
|
||||||
| Reverse Proxy | Traefik v3.6 | TLS termination, routing, middleware (rate-limit, headers, auth) |
|
|
||||||
| Container Runtime | Docker + Docker Compose | Service isolation and orchestration |
|
|
||||||
| Container Registry | GHCR (ghcr.io) | Docker image storage |
|
|
||||||
| Networking | Tailscale | Secure overlay network between VPS nodes |
|
|
||||||
| Cache | Redis (Alpine) | Session store, rate limit counters, caching |
|
|
||||||
| CI/CD | GitHub Actions | Build, test, deploy automation |
|
|
||||||
|
|
||||||
## Infrastructure
|
|
||||||
|
|
||||||
### Traefik Reverse Proxy
|
|
||||||
|
|
||||||
Traefik runs as the entry point for all HTTP/S traffic. It is configured via:
|
|
||||||
|
|
||||||
- **Static config**: `infra/traefik/traefik.yaml` — entry points, providers, plugins
|
|
||||||
- **Dynamic config**: `infra/traefik/dynamic/` — routers, services, middlewares, TLS
|
|
||||||
- **Docker provider**: Auto-discovers containers with `traefik.enable=true` labels
|
|
||||||
- **File provider**: Loads `apps.yaml` (routers/services), `middlewares.yaml`, `ssl.yaml`
|
|
||||||
|
|
||||||
Key middleware chains (`infra/traefik/dynamic/middlewares.yaml`):
|
|
||||||
|
|
||||||
- `secure-headers` — SSL redirect, HSTS, XSS protection, CSP
|
|
||||||
- `compress` — Gzip compression for responses over 256 bytes
|
|
||||||
- `rate-limit` — 100 avg / 50 burst requests
|
|
||||||
- `buffer` — 10MB request/response body limit
|
|
||||||
- `block-sensitive-paths` — blocks `.env`, `.git`, `/wp-admin` etc.
|
|
||||||
- `common-chain` — composes secure-headers + compress + retry + rate-limit + buffer
|
|
||||||
|
|
||||||
All services route through Traefik on port 443 (TLS), with automatic HTTP-to-HTTPS redirect.
|
|
||||||
|
|
||||||
### Docker Compose
|
|
||||||
|
|
||||||
Each service has its own Compose file under `infra/compose/`. All services join the `app-shared-net` external Docker network, enabling inter-service communication by container name.
|
|
||||||
|
|
||||||
Shared services:
|
|
||||||
|
|
||||||
- `infra/compose/shared.yml` — Redis (alias: `redis`)
|
|
||||||
- `infra/compose/traefik.yml` — Traefik reverse proxy
|
|
||||||
|
|
||||||
Service compose files are combined during deployment:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f traefik.yml -f shared.yml -f elysia.yml -f react.yml ... up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tailscale Networking
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph "Tailnet (100.64.0.0/10)"
|
|
||||||
IMRNES["imrnes (100.108.1.124)"]
|
|
||||||
ORANGE["orange (100.96.248.86)"]
|
|
||||||
ARCH["archlinux (100.114.19.66)"]
|
|
||||||
LAPTOP["laptop-2f6e1iph (100.86.195.29)"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "imrnes Services"
|
|
||||||
PG[(PostgreSQL)]
|
|
||||||
REDIS[Redis]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "orange Containers"
|
|
||||||
TRAEFIK[Traefik :443]
|
|
||||||
RUST_AUTH[rust-auth :3000]
|
|
||||||
ELYSIA[elysia-api :4092]
|
|
||||||
REACT[react-web :80]
|
|
||||||
SCRAPER[scraper-api :4091]
|
|
||||||
end
|
|
||||||
|
|
||||||
TRAEFIK --> RUST_AUTH
|
|
||||||
TRAEFIK --> ELYSIA
|
|
||||||
TRAEFIK --> REACT
|
|
||||||
TRAEFIK --> SCRAPER
|
|
||||||
|
|
||||||
RUST_AUTH -.->|Tailscale IP| PG
|
|
||||||
ELYSIA -.->|Tailscale IP| PG
|
|
||||||
RUST_AUTH -.->|Tailscale IP| REDIS
|
|
||||||
ELYSIA -.->|Tailscale IP| REDIS
|
|
||||||
|
|
||||||
style IMRNES fill:#3a7,color:#fff
|
|
||||||
style ORANGE fill:#37a,color:#fff
|
|
||||||
style ARCH fill:#773,color:#fff
|
|
||||||
style LAPTOP fill:#777,color:#fff
|
|
||||||
```
|
|
||||||
|
|
||||||
Container-to-Tailscale connectivity requires a systemd service that adds a route to the main routing table:
|
|
||||||
|
|
||||||
```
|
|
||||||
ip route add 100.64.0.0/10 dev tailscale0 table main
|
|
||||||
```
|
|
||||||
|
|
||||||
This is managed by `/etc/systemd/system/tailscale-routes.service` on the `orange` VPS.
|
|
||||||
|
|
||||||
## Data Flow
|
|
||||||
|
|
||||||
### Request Flow (Production)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant User as Browser/Client
|
|
||||||
participant DNS as Cloudflare DNS
|
|
||||||
participant Traefik as Traefik Proxy
|
|
||||||
participant App as Application Container
|
|
||||||
participant DB as PostgreSQL (imrnes via Tailscale)
|
|
||||||
participant Redis as Redis (imrnes via Tailscale)
|
|
||||||
|
|
||||||
User->>DNS: asepharyana.my.id
|
|
||||||
DNS->>User: A/AAAA record → orange VPS IP
|
|
||||||
User->>Traefik: HTTPS request :443
|
|
||||||
Traefik->>Traefik: TLS termination
|
|
||||||
Traefik->>Traefik: Middleware chain (headers, rate-limit, buffer)
|
|
||||||
Traefik->>App: HTTP reverse-proxy (internal network)
|
|
||||||
|
|
||||||
alt Database query
|
|
||||||
App->>DB: sqlx/Drizzle query via Tailscale
|
|
||||||
DB-->>App: Result set
|
|
||||||
else Cache lookup
|
|
||||||
App->>Redis: GET/SET via Tailscale
|
|
||||||
Redis-->>App: Cached value
|
|
||||||
end
|
|
||||||
|
|
||||||
App-->>Traefik: HTTP response
|
|
||||||
Traefik-->>User: HTTPS response
|
|
||||||
```
|
|
||||||
|
|
||||||
### CI/CD Pipeline
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
A[Push to main] --> B{Changed paths?}
|
|
||||||
B -->|apps/** or infra/docker/**| C[Build Docker Images]
|
|
||||||
B -->|infra/compose/**| D[Deploy to VPS]
|
|
||||||
B -->|apps/*/src/**/*.ts| E[Lint + TypeCheck]
|
|
||||||
|
|
||||||
C --> F[Push to GHCR]
|
|
||||||
F --> G[Update Compose tags]
|
|
||||||
G --> D
|
|
||||||
|
|
||||||
D --> H[SSH into VPS]
|
|
||||||
H --> I[Pull images]
|
|
||||||
I --> J[docker compose up -d]
|
|
||||||
|
|
||||||
subgraph "Build Phase"
|
|
||||||
C
|
|
||||||
F
|
|
||||||
G
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Deploy Phase"
|
|
||||||
D
|
|
||||||
H
|
|
||||||
I
|
|
||||||
J
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment Architecture
|
|
||||||
|
|
||||||
### Image Tags
|
|
||||||
|
|
||||||
- Every push to `main` triggers Docker builds for changed services
|
|
||||||
- Images are tagged with both `latest` and `sha-<short-sha>` (e.g., `sha-b0ef947`)
|
|
||||||
- Compose files are auto-updated to pin the new SHA tag
|
|
||||||
- This enables deterministic rollbacks by reverting the compose file change
|
|
||||||
|
|
||||||
### VPS Deployment
|
|
||||||
|
|
||||||
The `orange` VPS (Tailscale `100.96.248.86`) hosts all application containers:
|
|
||||||
|
|
||||||
1. GitHub Actions SSHes into the VPS
|
|
||||||
2. Production secrets are written as `.env`
|
|
||||||
3. The repo is synchronized via `git pull`
|
|
||||||
4. Changed compose files are detected by `git diff`
|
|
||||||
5. Docker images are pulled (with retry logic for transient failures)
|
|
||||||
6. Old containers are removed by `container_name`
|
|
||||||
7. `docker compose up -d` brings up the new containers
|
|
||||||
8. Traefik automatically detects the new containers via Docker provider
|
|
||||||
|
|
||||||
### Selective Deployment
|
|
||||||
|
|
||||||
The deploy workflow supports selective updates — if only `infra/compose/elysia.yml` changed, only `elysia-api` is pulled and recreated, avoiding disruption to other services.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph "Orange VPS"
|
|
||||||
DIR[/root/asepharyana-hub/]
|
|
||||||
ENV[.env]
|
|
||||||
COMPOSE[infra/compose/*.yml]
|
|
||||||
NET[app-shared-net]
|
|
||||||
|
|
||||||
DIR -->|git pull| COMPOSE
|
|
||||||
ENV -->|docker compose --env-file| COMPOSE
|
|
||||||
COMPOSE -->|docker compose pull| IMAGES[(GHCR Images)]
|
|
||||||
COMPOSE -->|docker compose up -d| CONT[Containers]
|
|
||||||
CONT --> NET
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "GitHub Actions"
|
|
||||||
BUILD[Build & Push]
|
|
||||||
DEPLOY[Deploy Workflow]
|
|
||||||
BUILD -->|trigger| DEPLOY
|
|
||||||
DEPLOY -->|SSH| DIR
|
|
||||||
end
|
|
||||||
|
|
||||||
IMAGES -->|registry| GHCR[ghcr.io/asepharyana]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Submodule Strategy
|
|
||||||
|
|
||||||
Each application lives in its own Git repository and is imported as a submodule into `apps/`. This approach:
|
|
||||||
|
|
||||||
- **Enables independent development** — each service can be developed, tested, and versioned separately
|
|
||||||
- **Pins exact commits** — the super-repository tracks exact submodule SHAs, enabling reproducible deployments
|
|
||||||
- **Supports `repository_dispatch`** — when a submodule receives a push, it can trigger the super-repository to build and deploy only that service
|
|
||||||
|
|
||||||
### Submodule Lifecycle
|
|
||||||
|
|
||||||
1. Developer pushes to a submodule (e.g., `apps/elysia`)
|
|
||||||
2. Submodule's GitHub Action dispatches `repository_dispatch` to the super-repo with the service name and new SHA
|
|
||||||
3. Super-repo detects the dispatch, waits for the SHA to be fetchable, then builds only that service
|
|
||||||
4. The compose manifest is updated and committed with the new SHA tag
|
|
||||||
5. The deploy workflow runs and updates only the changed containers
|
|
||||||
|
|
||||||
### Updating Submodules
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Update a single submodule to latest
|
|
||||||
cd apps/elysia
|
|
||||||
git checkout main
|
|
||||||
git pull
|
|
||||||
cd ../..
|
|
||||||
git add apps/elysia
|
|
||||||
git commit -m "chore(elysia): update submodule to latest"
|
|
||||||
|
|
||||||
# Update all submodules
|
|
||||||
git submodule update --remote --merge
|
|
||||||
```
|
|
||||||
|
|
||||||
## Service Mesh & Inter-Service Communication
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph LR
|
|
||||||
subgraph "External"
|
|
||||||
WWW[Internet]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Orange VPS"
|
|
||||||
TRAEFIK[Traefik :443]
|
|
||||||
|
|
||||||
subgraph "app-shared-net"
|
|
||||||
REACT[react-web<br/>:80]
|
|
||||||
ELYSIA[elysia-api<br/>:4092]
|
|
||||||
RUST_AUTH[rust-auth<br/>:3000]
|
|
||||||
SCRAPER[scraper-api<br/>:4091]
|
|
||||||
REDIS[redis<br/>:6379]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Imrnes VPS"
|
|
||||||
PG[(PostgreSQL<br/>:5432)]
|
|
||||||
REDIS_IMR[Redis<br/>:6379]
|
|
||||||
end
|
|
||||||
|
|
||||||
WWW -->|HTTPS| TRAEFIK
|
|
||||||
TRAEFIK --> REACT
|
|
||||||
TRAEFIK --> ELYSIA
|
|
||||||
TRAEFIK --> RUST_AUTH
|
|
||||||
TRAEFIK --> SCRAPER
|
|
||||||
|
|
||||||
ELYSIA -->|Tailscale| PG
|
|
||||||
RUST_AUTH -->|Tailscale| PG
|
|
||||||
ELYSIA -->|internal| REDIS
|
|
||||||
RUST_AUTH -->|internal| REDIS
|
|
||||||
ELYSIA -->|Tailscale| REDIS_IMR
|
|
||||||
RUST_AUTH -->|Tailscale| REDIS_IMR
|
|
||||||
```
|
|
||||||
|
|
||||||
## Observability
|
|
||||||
|
|
||||||
- **Prometheus metrics**: Available on rust-auth via `axum-prometheus`
|
|
||||||
- **Traefik access logs**: JSON format, logged at INFO level
|
|
||||||
- **Dashboard**: Traefik dashboard at `traefik.asepharyana.my.id` (secured)
|
|
||||||
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2026-08-02]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Infra overhaul**: Docker + Traefik dihapus dari produksi → Caddy 2.11.4 (reverse proxy, auto-TLS LE, HTTP/3) + Nix/systemd services.
|
||||||
|
- **Port migration**: semua service pindah ke port 4000-an (hub 4003, tools 4007/4008, scraper 4091, llm-api 4010, dll).
|
||||||
|
- **DB via PgBouncer pool**: semua service konek ke imrnes 100.121.180.82:6432 (bukan :5432 langsung).
|
||||||
|
- **Secrets**: Bitwarden Secrets Manager (BWS) sebagai central secret store, wrapper bws-exec.
|
||||||
|
- **Flake**: dibatasi x86_64-linux (nixpkgs 26.11 drop darwin).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
@@ -12,8 +22,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Restructured the repository into a lightweight hub repo with standalone app submodules.
|
- Restructured the repository into a lightweight hub repo with standalone app submodules.
|
||||||
- Simplified root tooling to plain `package.json` scripts and per-service commands.
|
- Simplified root tooling to plain `package.json` scripts and per-service commands.
|
||||||
- Kept infrastructure, deployment workflows, and documentation in the root hub repo.
|
- Kept infrastructure, deployment workflows, and documentation in the root hub repo.
|
||||||
|
- Cleaned up Traefik SSL config: removed legacy `asephstech`/`asephscloud` cert references, synced volume mounts, fixed `api.insecure`.
|
||||||
|
- Pruned `.env.example` from 145 legacy vars (Firebase, Discord, Coolify, Portainer, YouTube, etc.) to 30 focused vars.
|
||||||
|
- Optimized `scraper.Dockerfile`: removed Node.js and Chromium from runtime image.
|
||||||
|
- Simplified CI/CD workflows: removed orphan container reference, commented code blocks.
|
||||||
|
- Cleaned up scripts: removed stale MySQL config, fixed package references, simplified update-deps.
|
||||||
|
- Added NATS + JetStream message broker infrastructure (`infra/compose/nats.yml`).
|
||||||
|
- Added Dapr runtime infrastructure: placement service, sidecar pattern, pub/sub + state store components.
|
||||||
|
- Integrated Dapr sidecar into scraper service (`infra/compose/scraper.yml`).
|
||||||
|
- Updated deployment order: shared → NATS → Dapr → Traefik → apps.
|
||||||
|
- Added `docs/add-dapr-service.md` guide for adding Dapr to new services.
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- Removed deprecated services from apps, compose files, Dockerfiles, Traefik routes, and workflows.
|
- Removed deprecated services from apps, compose files, Dockerfiles, Traefik routes, and workflows.
|
||||||
- Removed stale monorepo orchestration configs and hook tooling from the root repo.
|
- Removed stale monorepo orchestration configs and hook tooling from the root repo.
|
||||||
|
- Removed `.nvmrc` (duplicate of `.node-version`), `renovate.json` (using dependabot).
|
||||||
|
- Removed stale documentation: `dependency-map.md`, `observability.md`, `handoff-log.jsonl`, `quality-gates.json`, `workflow-state.json`.
|
||||||
|
- Removed deprecated `docs/superpowers/` design docs.
|
||||||
|
- Removed `infra/config/mysql/` (no active MySQL service) and `docs/config/squid.conf.archived`.
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Repository Overview
|
||||||
|
|
||||||
|
Asepharyana Hub is a **hub monorepo** for Asep Haryana Saputra's portfolio ecosystem. Application services live in separate repos imported as Git submodules under `apps/`. Production infrastructure: Caddy reverse proxy + Nix/systemd services (Docker/Traefik removed 2026-08-02; legacy configs under `infra/` marked LEGACY).
|
||||||
|
|
||||||
|
```
|
||||||
|
asepharyana-hub/
|
||||||
|
├── apps/ # Git submodules — each app is its own repo
|
||||||
|
│ ├── hub/ # Personal portfolio SPA (asepharyana-hub-hub)
|
||||||
|
│ └── scraper/ # Rust scraper API (asepharyana-hub-scraper)
|
||||||
|
├── docs/ # ADRs, deployment guide, new-app guide
|
||||||
|
├── infra/
|
||||||
|
│ ├── compose/ # Docker Compose files (LEGACY — Docker dihapus)
|
||||||
|
│ ├── dapr/ # Dapr config + component definitions
|
||||||
|
│ ├── docker/ # Dockerfiles (LEGACY)
|
||||||
|
│ └── traefik/ # Reverse proxy config (static + dynamic)
|
||||||
|
├── scripts/ # Utility scripts (cleanup, update-deps, git hooks)
|
||||||
|
└── .github/workflows/ # CI/CD pipelines
|
||||||
|
```
|
||||||
|
|
||||||
|
### Submodule Strategy
|
||||||
|
- Each app in `apps/` is a separate Git repo imported as a submodule. Code changes happen in the submodule repo, not here.
|
||||||
|
- Submodule pointers are updated by CI/CD (via `repository_dispatch` or manual commit).
|
||||||
|
- Current submodules:
|
||||||
|
- `apps/hub` → `asepharyana/asepharyana-hub-hub`
|
||||||
|
- `apps/scraper` → `asepharyana/asepharyana-hub-scraper`
|
||||||
|
- `apps/llm-api` → `asepharyana/asepharyana-hub-llm-api`
|
||||||
|
- `apps/tools` → `asepharyana/asepharyana-hub-tools`.
|
||||||
|
|
||||||
|
### Infrastructure Stack
|
||||||
|
- **Caddy 2.11.4** — reverse proxy, TLS termination (auto-LE), HTTP/3, zstd/gzip, keep-alive tuning (`/etc/caddy/Caddyfile`, ref `infra/caddy/Caddyfile.prod`)
|
||||||
|
- **NATS + JetStream** — message broker with persistent streaming
|
||||||
|
- **Dapr** — sidecar runtime (pub/sub abstraction, state management, service invocation)
|
||||||
|
- **Redis (Alpine)** — cache, session store, Dapr state store & pub/sub backend
|
||||||
|
- **Prometheus** — metrics backend with `file_sd_configs` target files.
|
||||||
|
- **Jaeger** — distributed tracing backend (all-in-one), OTLP receiver
|
||||||
|
- **Tailscale** — secure overlay network between VPS nodes (PostgreSQL on `imrnes`, containers on `orangevps`)
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
- **Hub dashboard** at `/dashboard` (Next.js client page, auto-refresh 15s)
|
||||||
|
- **Dashboard API** at `/api/dashboard` — returns JSON with systemd services, Jaeger traces, Prometheus metrics (RPS, latency, errors, node CPU/RAM/Disk)
|
||||||
|
- **Prometheus** scrapes node-exporter + app metrics endpoints
|
||||||
|
|
||||||
|
### Networking
|
||||||
|
- All services run as Nix/systemd units; inter-service via 127.0.0.1:<port>.
|
||||||
|
- Caddy handles all external HTTP/S traffic on port 443 (and HTTP/3 UDP).
|
||||||
|
- Cross-VPS traffic (DB, Redis) goes through Tailscale (`100.64.0.0/10`). Container-to-Tailscale connectivity requires a route in the main routing table (managed by `tailscale-routes.service`).
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make init-submodules # Initialize submodules after clone
|
||||||
|
make dev # Start shared dev infrastructure (Redis)
|
||||||
|
make update-submodules # Update all submodules to latest
|
||||||
|
|
||||||
|
bun run check # Biome lint + format + write
|
||||||
|
bun run ci # Biome CI mode (no writes, exit code on issues)
|
||||||
|
bun run format # Format only
|
||||||
|
bun run lint # Lint only
|
||||||
|
|
||||||
|
# Nix build (produksi): nix build .#default --impure --option sandbox false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validate YAML
|
||||||
|
```bash
|
||||||
|
python -c "import pathlib, yaml; [yaml.safe_load(open(p)) for p in pathlib.Path('infra').rglob('*.yml')]"
|
||||||
|
for f in infra/compose/*.yml; do docker compose -f "$f" config >/dev/null && echo "OK $f"; done
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD Workflows
|
||||||
|
|
||||||
|
| Workflow | Trigger | Action |
|
||||||
|
|----------|---------|--------|
|
||||||
|
| `lint.yml` | PR/push to main touching `*.json`, `*.js`, `biome.json` | `bun run ci` (Biome lint) |
|
||||||
|
| `deploy.yml` | Push to main | nix build → nix copy ssh:// → systemctl restart |
|
||||||
|
| `docker-build-push.yml` | LEGACY (Docker dihapus) | LEGACY |
|
||||||
|
| `security.yml` | PR to main + weekly Monday | CodeQL analysis (Rust) |
|
||||||
|
| `update-submodule.yml` | `repository_dispatch` | Update submodule pointer in hub repo |
|
||||||
|
|
||||||
|
### Deployment Order
|
||||||
|
1. `shared.yml` (Redis)
|
||||||
|
2. `nats.yml` (NATS + JetStream)
|
||||||
|
3. `dapr.yml` (Dapr placement)
|
||||||
|
4. `traefik.yml` (Reverse proxy)
|
||||||
|
5. Service compose files (app + Dapr sidecar)
|
||||||
|
|
||||||
|
### Secrets Required for Deploy
|
||||||
|
`SSH_PRIVATE_KEY`, `VPS_HOST`, `VPS_USER`, `VPS_TARGET_DIR`, `ENV_FILE_PRODUCTION`
|
||||||
|
|
||||||
|
## Infrastructure Patterns
|
||||||
|
|
||||||
|
### Compose File Pattern
|
||||||
|
Each service gets one compose file. Containers join `app-shared-net` with a `container_name` alias for DNS. The network is declared `external: true`.
|
||||||
|
|
||||||
|
### Dapr Sidecar Pattern
|
||||||
|
Each app gets a companion `daprd` sidecar container. Dapr components (pubsub, statestore) are mounted from `infra/dapr/components/`. The sidecar communicates with NATS for pub/sub and Dapr placement for actor coordination.
|
||||||
|
|
||||||
|
### Caddy Routing
|
||||||
|
- Site blocks in `/etc/caddy/Caddyfile` (ref `infra/caddy/Caddyfile.prod`)
|
||||||
|
- Subdomain pattern: `<service>.asepharyana.my.id` and `<service>.asepharya.web.id`
|
||||||
|
- Auto-TLS via Let's Encrypt
|
||||||
|
- Shared handler snippet `(proxy)`: `encode zstd gzip` + security headers + keep-alive tuning
|
||||||
|
|
||||||
|
### Image Tagging
|
||||||
|
- `sha-<short-sha>` — immutable, for deterministic rollbacks
|
||||||
|
- `latest` — mutable, for convenience
|
||||||
|
- Registry: `ghcr.io/asepharyana/asepharyana-hub/<service>`
|
||||||
|
- Build cache: `sha-<short>-buildcache` (registry-based caching)
|
||||||
|
|
||||||
|
## Adding a New Service
|
||||||
|
|
||||||
|
1. Create a separate repo for the app code
|
||||||
|
2. Add as submodule: `git submodule add <url> apps/<name>`
|
||||||
|
3. Create Nix flake package + systemd unit
|
||||||
|
4. Create compose file in `infra/compose/` (app + Dapr sidecar)
|
||||||
|
5. Add Caddy site block in `/etc/caddy/Caddyfile`
|
||||||
|
6. Add build job in `.github/workflows/docker-build-push.yml`
|
||||||
|
7. See `docs/add-new-app.md` for full guide
|
||||||
|
|
||||||
|
## Commit Convention
|
||||||
|
|
||||||
|
Format: `<type>(<scope>): <description>`
|
||||||
|
|
||||||
|
Types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `ci`, `perf`, `style`
|
||||||
|
Scopes: `scraper`, `infra`, `ci`, `dapr`, `nats`, `docs`, `deps`, `scripts`, `root`
|
||||||
|
|
||||||
|
Scope is required. Use imperative mood. No period at end of subject line. Co-Authored-By footer for AI-generated commits.
|
||||||
+15
-67
@@ -14,9 +14,8 @@
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **Git** with LFS support
|
- **Git** with LFS support
|
||||||
- **Node.js** >= 22.11.0 (via `.node-version` or `.nvmrc`)
|
- **Node.js** >= 22.11.0 (via `.node-version`)
|
||||||
- **Bun** >= 1.3.11 (package manager)
|
- **Bun** >= 1.3.11 (package manager)
|
||||||
- **Rust** >= 1.89.0 (for Rust services)
|
|
||||||
- **Docker** and **Docker Compose** (for shared infrastructure)
|
- **Docker** and **Docker Compose** (for shared infrastructure)
|
||||||
|
|
||||||
## Local Setup
|
## Local Setup
|
||||||
@@ -40,18 +39,14 @@ This checks out all submodules at the pinned commit (not `main`). The submodules
|
|||||||
|
|
||||||
| Path | Remote |
|
| Path | Remote |
|
||||||
| ---------------- | --------------------------------------- |
|
| ---------------- | --------------------------------------- |
|
||||||
| `apps/elysia` | `asepharyana/asepharyana-hub-elysia` |
|
|
||||||
| `apps/scraper` | `asepharyana/asepharyana-hub-scraper` |
|
| `apps/scraper` | `asepharyana/asepharyana-hub-scraper` |
|
||||||
| `apps/react` | `asepharyana/asepharyana-hub-react` |
|
|
||||||
| `apps/rust-auth` | `asepharyana/asepharyana-hub-rust-auth` |
|
|
||||||
|
|
||||||
### 3. Install Dependencies per Service
|
### 3. Install Dependencies per Service
|
||||||
|
|
||||||
Install dependencies for TypeScript/Bun services:
|
Install dependencies for TypeScript/Bun services:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd apps/elysia && bun install && cd ../..
|
cd apps/scraper && bun install && cd ../..
|
||||||
cd apps/react && npm install && cd ../..
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Start Shared Infrastructure
|
### 4. Start Shared Infrastructure
|
||||||
@@ -75,76 +70,42 @@ Key variables to configure:
|
|||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
| -------------- | ----------------------------------------------------- |
|
| -------------- | ----------------------------------------------------- |
|
||||||
| `DATABASE_URL` | PostgreSQL connection (Tailscale IP to `imrnes` VPS) |
|
| `DATABASE_URL` | PostgreSQL connection (Tailscale IP to `imrnes` VPS) |
|
||||||
| `REDIS_URL` | Redis connection (`redis://localhost:6379` for local) |
|
|
||||||
| `JWT_SECRET` | JWT signing secret |
|
|
||||||
| `GITHUB_TOKEN` | GitHub personal access token |
|
| `GITHUB_TOKEN` | GitHub personal access token |
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
### Running Services
|
### Running Services
|
||||||
|
|
||||||
**Rust API (rust-auth):**
|
Refer to each service's own documentation for setup and development instructions.
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/rust-auth
|
|
||||||
cargo run
|
|
||||||
```
|
|
||||||
|
|
||||||
**Elysia API (elysia):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/elysia
|
|
||||||
bun run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
**React Frontend (react):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/react
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### API Documentation
|
### API Documentation
|
||||||
|
|
||||||
- Rust OpenAPI: `http://localhost:4091/docs`
|
Refer to each service's own documentation for API docs and endpoints.
|
||||||
- Elysia Swagger: `http://localhost:4092/docs`
|
|
||||||
- Elysia AsyncAPI: `http://localhost:4092/docs-ws`
|
|
||||||
|
|
||||||
## Coding Standards
|
## Coding Standards
|
||||||
|
|
||||||
### Linting
|
### Linting
|
||||||
|
|
||||||
- **ESLint** with `@antfu/eslint-config` for TypeScript/JavaScript
|
- **Biome** for TypeScript/JavaScript formatting and linting
|
||||||
- **Cargo Clippy** for Rust
|
|
||||||
|
|
||||||
Run linting:
|
Run linting:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# TypeScript/JavaScript
|
# TypeScript/JavaScript
|
||||||
eslint . --no-error-on-unmatched-pattern
|
bun run check
|
||||||
|
|
||||||
# Rust specific
|
|
||||||
cd apps/rust-auth && cargo clippy -- -D warnings
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Formatting
|
### Formatting
|
||||||
|
|
||||||
- **Prettier** for TypeScript/JavaScript/Markdown (config in `.prettierrc`)
|
- **Biome** for TypeScript/JavaScript
|
||||||
- Single quotes, 100 print width, 2-space indent, trailing commas
|
|
||||||
- **Cargo fmt** for Rust
|
|
||||||
- **EditorConfig** for general formatting (`.editorconfig`)
|
- **EditorConfig** for general formatting (`.editorconfig`)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Prettier
|
# Format all
|
||||||
prettier --write .
|
bun run format
|
||||||
|
|
||||||
# Rust
|
|
||||||
cd apps/rust-auth && cargo fmt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Rust Configuration
|
|
||||||
|
|
||||||
Rust services use edition `2024` with stable toolchain (nightly features may be used).
|
|
||||||
|
|
||||||
## Commit Message Format
|
## Commit Message Format
|
||||||
|
|
||||||
@@ -177,18 +138,15 @@ This project enforces **Conventional Commits** for all commit messages.
|
|||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
```
|
```
|
||||||
feat(rust-auth): add OAuth2 Google login flow
|
feat(scraper): add new data source integration
|
||||||
fix(elysia): handle null JWT payload in auth middleware
|
chore: update biome config to v10
|
||||||
chore: update eslint config to v10
|
|
||||||
docs: add API endpoint documentation for scraper
|
docs: add API endpoint documentation for scraper
|
||||||
refactor(react): extract Header component from App
|
|
||||||
test(elysia): add unit tests for rate limiter
|
|
||||||
ci: migrate to CodeQL v3
|
ci: migrate to CodeQL v3
|
||||||
```
|
```
|
||||||
|
|
||||||
### Scopes
|
### Scopes
|
||||||
|
|
||||||
Common scopes: `rust-auth`, `elysia`, `react`, `scraper`, `infra`, `ci`, `deps`
|
Common scopes: `scraper`, `infra`, `ci`, `deps`
|
||||||
|
|
||||||
## Pull Request Process
|
## Pull Request Process
|
||||||
|
|
||||||
@@ -202,8 +160,7 @@ Common scopes: `rust-auth`, `elysia`, `react`, `scraper`, `infra`, `ci`, `deps`
|
|||||||
3. **Run checks locally** before pushing:
|
3. **Run checks locally** before pushing:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd apps/react && npx tsc --noEmit
|
bun run check
|
||||||
eslint . --no-error-on-unmatched-pattern
|
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Push and open a PR** against `main`. CI will automatically run:
|
4. **Push and open a PR** against `main`. CI will automatically run:
|
||||||
@@ -218,7 +175,7 @@ Common scopes: `rust-auth`, `elysia`, `react`, `scraper`, `infra`, `ci`, `deps`
|
|||||||
- Updates compose manifests to use the new SHA tags
|
- Updates compose manifests to use the new SHA tags
|
||||||
|
|
||||||
6. **Deployment Pipeline** triggers after a successful Docker build:
|
6. **Deployment Pipeline** triggers after a successful Docker build:
|
||||||
- SSHes into the VPS (`orange`, Tailscale IP `100.96.248.86`)
|
- SSHes into the VPS (`orangevps`, Tailscale IP `100.79.111.61`)
|
||||||
- Pulls updated Docker images
|
- Pulls updated Docker images
|
||||||
- Recreates only the changed containers
|
- Recreates only the changed containers
|
||||||
- All services share the `app-shared-net` Docker network
|
- All services share the `app-shared-net` Docker network
|
||||||
@@ -227,13 +184,4 @@ Common scopes: `rust-auth`, `elysia`, `react`, `scraper`, `infra`, `ci`, `deps`
|
|||||||
|
|
||||||
## Adding a New Service
|
## Adding a New Service
|
||||||
|
|
||||||
See `docs/add-new-app.md` for the complete step-by-step guide. In summary:
|
See `docs/add-new-app.md` for the complete step-by-step guide.
|
||||||
|
|
||||||
1. Create the app in `apps/<name>`
|
|
||||||
2. Add it as a Git submodule in `.gitmodules`
|
|
||||||
3. Register it in `infra/compose/<name>.yml`
|
|
||||||
4. Add a Dockerfile at `infra/docker/<name>.Dockerfile`
|
|
||||||
5. Add Traefik routing config in `infra/traefik/dynamic/apps.yaml`
|
|
||||||
6. Add CI entries in `.github/workflows/docker-build-push.yml`
|
|
||||||
7. Add compose file to the deploy script in `deploy-docker.yml`
|
|
||||||
8. Add any required GitHub secrets for the service
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
.PHONY: help dev lint format test clean update-submodules deploy init-submodules status
|
|
||||||
|
|
||||||
SHELL := /bin/bash
|
|
||||||
|
|
||||||
help: ## Show this help
|
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
|
||||||
|
|
||||||
dev: ## Start development infrastructure (Redis etc.)
|
|
||||||
docker compose -f infra/compose/shared.yml up -d
|
|
||||||
|
|
||||||
lint: ## Run Biome linter
|
|
||||||
biome lint .
|
|
||||||
|
|
||||||
format: ## Format code with Biome
|
|
||||||
biome format --write .
|
|
||||||
|
|
||||||
clean: ## Clean build artifacts
|
|
||||||
rm -rf apps/*/dist apps/*/.next apps/*/target 2>/dev/null || true
|
|
||||||
rm -rf node_modules 2>/dev/null || true
|
|
||||||
|
|
||||||
update-submodules: ## Update all git submodules to latest remote
|
|
||||||
git submodule update --remote --merge --recursive
|
|
||||||
|
|
||||||
deploy: ## Deploy to VPS (triggers GitHub Actions)
|
|
||||||
@echo "Push to main to trigger deployment, or run:"
|
|
||||||
@echo " gh workflow run deploy-docker.yml"
|
|
||||||
|
|
||||||
init-submodules: ## Initialize all submodules
|
|
||||||
git submodule update --init --recursive
|
|
||||||
|
|
||||||
status: ## Show submodule status
|
|
||||||
git submodule status
|
|
||||||
@@ -1,201 +1,56 @@
|
|||||||
# Asepharyana Hub
|
# Asepharyana Infra
|
||||||
|
|
||||||
Hub repo untuk ekosistem portfolio dan layanan pendukung milik Asep Haryana Saputra.
|
Reverse-proxy & infrastructure config for [orangevps](https://asepharyana.my.id) (45.127.35.244).
|
||||||
Aplikasi dipisah sebagai submodule agar frontend, API, dan service pendukung bisa dikembangkan serta di-deploy secara independen.
|
|
||||||
|
|
||||||
## Services
|
> **Status (2026-08-28):** Repo ini dulunya monorepo `asepharyana-hub` dengan submodule aplikasi.
|
||||||
|
> Kini **murni repo infra**: Caddy reverse proxy (source of truth), firewall, drop-in systemd,
|
||||||
|
> dan docs. Build + deploy tiap aplikasi pindah ke repo masing-masing (self-contained CI).
|
||||||
|
|
||||||
| Service | Path | Default Local Port | Notes |
|
## Repositori Aplikasi (self-contained build & deploy)
|
||||||
| :---------- | :--------------- | :----------------- | :---------------------------------------------------------------------------- |
|
|
||||||
| Rust API | `apps/rust-auth` | `4091` | API utama (Axum + SeaORM), scraping, image proxy/cache, metrics, OpenAPI docs |
|
|
||||||
| Elysia API | `apps/elysia` | `4092` | API realtime/auth/chat/quiz (Elysia + Bun + Drizzle + Redis) |
|
|
||||||
| React Web | `apps/react` | `3000` | Frontend React/Vite |
|
|
||||||
| Scraper | `apps/scraper` | — | Web scraper service |
|
|
||||||
|
|
||||||
## Infrastructure
|
| Repo | Deskripsi | Deploy unit |
|
||||||
|
|------|-----------|-------------|
|
||||||
|
| [`asepharyana/hub`](https://github.com/asepharyana/hub) | Portfolio SPA (Next.js, port 4003, dashboard) | `hub` |
|
||||||
|
| [`asepharyana/scraper`](https://github.com/asepharyana/scraper) | Rust/Axum scraper API (port 4091) | `scraper` |
|
||||||
|
| [`asepharyana/llm-api`](https://github.com/asepharyana/llm-api) | Rust LLM API (llama.cpp, port 8080) | `llm-api` |
|
||||||
|
|
||||||
File compose berada di `infra/compose/`:
|
Tiap repo punya `flake.nix` + `.github/workflows/deploy.yml` sendiri:
|
||||||
|
`nix build .#<pkg>` → **push ke Attic binary cache** (`attic.asepharyana.my.id/asepharyana`) → VPS substitute via `nix-store --realise` → `nix-env --profile` → `systemctl restart`.
|
||||||
|
Push ke `main` (atau `workflow_dispatch`) langsung deploy; tidak ada lagi pointer submodule.
|
||||||
|
|
||||||
- `traefik.yml`: reverse proxy Traefik untuk semua layanan.
|
## Infra di Repo Ini
|
||||||
- `shared.yml`: Redis.
|
|
||||||
- `rust-auth.yml`, `elysia.yml`, `react.yml`, `scraper.yml`: manifest deploy per service (image GHCR bertag SHA).
|
|
||||||
|
|
||||||
Dockerfile per service berada di `infra/docker/`.
|
| Path | Isi |
|
||||||
|
|------|-----|
|
||||||
|
| `infra/caddy/Caddyfile.prod` | **Source of truth** `/etc/caddy/Caddyfile` (auto-deploy via CI) |
|
||||||
|
| `infra/firewall/firewall.sh` | deny-by-default iptables (SSH/80/443/4013/Tailscale/TCPShield) |
|
||||||
|
| `infra/firewall/99-*.conf` | sysctl hardenings |
|
||||||
|
| `infra/prometheus/targets.yml` | file_sd targets |
|
||||||
|
| `infra/systemd/scraper-otel.conf` | drop-in OTEL untuk scraper service |
|
||||||
|
| `docs/` | arsitektur + operasional (VPS) |
|
||||||
|
|
||||||
## Docker Image Builds
|
## CI/CD
|
||||||
|
|
||||||
Build image via Dockerfile:
|
| Workflow | Trigger | Aksi |
|
||||||
|
|----------|---------|------|
|
||||||
|
| `caddy-deploy.yml` | push main menyentuh `infra/**`, atau manual | sync `Caddyfile.prod` → `/etc/caddy/Caddyfile` → reload → verifikasi rute |
|
||||||
|
|
||||||
|
## Local Setup / Snapshot VPS
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -f infra/docker/rust.Dockerfile -t rust-auth:latest .
|
# Clone infra repo
|
||||||
docker build -f infra/docker/elysia.Dockerfile -t elysia-api:latest .
|
git clone https://github.com/asepharyana/infra.git
|
||||||
docker build -f infra/docker/react.Dockerfile -t react-web:latest .
|
# Diff config live vs repo
|
||||||
docker build -f infra/docker/scraper.Dockerfile -t scraper-api:latest .
|
diff /etc/caddy/Caddyfile infra/caddy/Caddyfile.prod
|
||||||
|
# Koneksi VPS (public)
|
||||||
|
ssh code@45.127.35.244
|
||||||
```
|
```
|
||||||
|
|
||||||
Tag and push:
|
## Menambahkan Service Baru / Subdomain
|
||||||
|
|
||||||
```bash
|
1. Aplikasi punya repo sendiri + `deploy.yml` (lihat template di repo app yang ada).
|
||||||
SHORT_SHA=$(git rev-parse --short HEAD)
|
2. Registrasi unit systemd di VPS (manual/ops) → app jalan di port lokal.
|
||||||
|
3. Tambah site block di `infra/caddy/Caddyfile.prod` (pola `import proxy <port>`) → push → CI reload Caddy.
|
||||||
|
4. (Opsional) Tambah unit ke `MONITORED_UNITS` dashboard hub di repo `asepharyana/hub`.
|
||||||
|
|
||||||
docker tag rust-auth:latest ghcr.io/asepharyana/asepharyana-hub/rust-auth:sha-$SHORT_SHA
|
Lihat `docs/add-new-app.md` untuk detail.
|
||||||
docker push ghcr.io/asepharyana/asepharyana-hub/rust-auth:sha-$SHORT_SHA
|
|
||||||
# repeat for elysia-api, react-web, scraper-api
|
|
||||||
```
|
|
||||||
|
|
||||||
## Local Development
|
|
||||||
|
|
||||||
### 1) Jalankan dependency bersama
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f infra/compose/shared.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2) Jalankan service yang dibutuhkan
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Rust API
|
|
||||||
cd apps/rust-auth
|
|
||||||
cargo run
|
|
||||||
|
|
||||||
# Elysia API
|
|
||||||
cd apps/elysia
|
|
||||||
bun install
|
|
||||||
bun run dev
|
|
||||||
|
|
||||||
# React web
|
|
||||||
cd apps/react
|
|
||||||
npm install
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Docs and Monitoring
|
|
||||||
|
|
||||||
- Rust OpenAPI: `/docs`
|
|
||||||
- Elysia Swagger: `/docs`
|
|
||||||
- Elysia AsyncAPI viewer: `/docs-ws`
|
|
||||||
|
|
||||||
## Deployment Notes
|
|
||||||
|
|
||||||
- Pipeline memakai image tag berbasis commit SHA (`sha-<short-sha>`), bukan `latest`.
|
|
||||||
- Deploy Compose sekarang mencakup `infra/compose/*.yml` dan `deploy-docker.yml` akan berjalan langsung ketika `infra/compose/**` berubah.
|
|
||||||
|
|
||||||
## Networking & Tailscale
|
|
||||||
|
|
||||||
### Arsitektur
|
|
||||||
|
|
||||||
Semua VPS terhubung via **Tailscale**. Setiap VPS punya IP Tailscale dan service berkomunikasi antar VPS melalui Tailscale network (`100.64.0.0/10`).
|
|
||||||
|
|
||||||
| VPS | Tailscale IP | Service |
|
|
||||||
| :---------------- | :-------------- | :------------------------------------- |
|
|
||||||
| `imrnes` | `100.108.1.124` | PostgreSQL (`hub`), Redis |
|
|
||||||
| `orange` | `100.96.248.86` | App containers (Traefik, 9Router, dll) |
|
|
||||||
| `archlinux` | `100.114.19.66` | _(development machine)_ |
|
|
||||||
| `laptop-2f6e1iph` | `100.86.195.29` | _(offline)_ |
|
|
||||||
|
|
||||||
### Container → Tailscale Connectivity
|
|
||||||
|
|
||||||
Docker containers di bridge network (`app-shared-net`) **tidak otomatis bisa access Tailscale IPs** karena Tailscale menggunakan **custom policy routing** (routes di `table 52`, bukan `main` table).
|
|
||||||
|
|
||||||
#### Fix: Tailscale Route di Main Table
|
|
||||||
|
|
||||||
Agar container bisa reach Tailscale IPs (untuk DB, Redis, dll), tambahkan route ke `main` routing table:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Manual (hilang setelah reboot)
|
|
||||||
ip route add 100.64.0.0/10 dev tailscale0 table main
|
|
||||||
|
|
||||||
# Persistent (systemd service)
|
|
||||||
# Sudah dikonfigurasi sebagai /etc/systemd/system/tailscale-routes.service
|
|
||||||
# Service ini berjalan otomatis setelah tailscaled start
|
|
||||||
systemctl enable tailscale-routes.service
|
|
||||||
systemctl start tailscale-routes.service
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Environment Variables
|
|
||||||
|
|
||||||
Service yang connect ke Tailscale IP:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# PostgreSQL di imrnes
|
|
||||||
DATABASE_URL=postgres://user:pass@100.108.1.124:5432/dbname
|
|
||||||
|
|
||||||
# Redis di imrnes
|
|
||||||
REDIS_URL=redis://100.108.1.124:6379
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Persistent Systemd Service
|
|
||||||
|
|
||||||
File: `/etc/systemd/system/tailscale-routes.service`
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[Unit]
|
|
||||||
Description=Add Tailscale routes to main routing table
|
|
||||||
After=tailscaled.service
|
|
||||||
Requires=tailscaled.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/bin/bash -c '/usr/sbin/ip route add 100.64.0.0/10 dev tailscale0 table main 2>/dev/null || /usr/sbin/ip route replace 100.64.0.0/10 dev tailscale0 table main'
|
|
||||||
RemainAfterExit=yes
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
```
|
|
||||||
|
|
||||||
Install & enable:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo tee /etc/systemd/system/tailscale-routes.service > /dev/null << 'EOF'
|
|
||||||
[Unit]
|
|
||||||
Description=Add Tailscale routes to main routing table
|
|
||||||
After=tailscaled.service
|
|
||||||
Requires=tailscaled.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/bin/bash -c '/usr/sbin/ip route add 100.64.0.0/10 dev tailscale0 table main 2>/dev/null || /usr/sbin/ip route replace 100.64.0.0/10 dev tailscale0 table main'
|
|
||||||
RemainAfterExit=yes
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable tailscale-routes.service
|
|
||||||
sudo systemctl start tailscale-routes.service
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Troubleshooting
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Cek Tailscale peers
|
|
||||||
tailscale status
|
|
||||||
|
|
||||||
# Cek route table 52 (Tailscale internal)
|
|
||||||
ip route show table 52
|
|
||||||
|
|
||||||
# Cek route table main (yang dipakai container)
|
|
||||||
ip route show table main | grep 100.
|
|
||||||
|
|
||||||
# Test connectivity dari dalam container
|
|
||||||
docker exec <container> node -e "
|
|
||||||
const net = require('net');
|
|
||||||
const c = new net.Socket();
|
|
||||||
c.setTimeout(5000);
|
|
||||||
c.connect(5432, '100.108.1.124', () => { console.log('OK'); c.end(); });
|
|
||||||
c.on('error', e => { console.log('FAIL:', e.code); });
|
|
||||||
c.on('timeout', () => { console.log('TIMEOUT'); c.destroy(); });
|
|
||||||
"
|
|
||||||
|
|
||||||
# Cek service tailscale-routes
|
|
||||||
systemctl status tailscale-routes.service
|
|
||||||
```
|
|
||||||
|
|
||||||
## Menambahkan Aplikasi Baru
|
|
||||||
|
|
||||||
Panduan langkah demi langkah untuk menambahkan aplikasi baru ada di `docs/add-new-app.md`.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
-1
Submodule apps/elysia deleted from b8f4b5806e
-1
Submodule apps/react deleted from 124d2f88bd
Submodule apps/rust-auth deleted from 6e695f07e8
-1
Submodule apps/scraper deleted from 80c96eaa42
-73
@@ -1,73 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://biomejs.dev/schemas/2.5.3/schema.json",
|
|
||||||
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"preset": "recommended",
|
|
||||||
"a11y": {
|
|
||||||
"useButtonType": "warn",
|
|
||||||
"useIframeTitle": "warn",
|
|
||||||
"noSvgWithoutTitle": "warn",
|
|
||||||
"useAltText": "warn"
|
|
||||||
},
|
|
||||||
"complexity": {
|
|
||||||
"noForEach": "off",
|
|
||||||
"noStaticOnlyClass": "off",
|
|
||||||
"noThisInStatic": "off",
|
|
||||||
"useOptionalChain": "off"
|
|
||||||
},
|
|
||||||
"suspicious": {
|
|
||||||
"noArrayIndexKey": "warn",
|
|
||||||
"noConsole": "off",
|
|
||||||
"noExplicitAny": "warn"
|
|
||||||
},
|
|
||||||
"correctness": {
|
|
||||||
"noUnusedVariables": "error",
|
|
||||||
"useParseIntRadix": "off"
|
|
||||||
},
|
|
||||||
"style": {
|
|
||||||
"noNonNullAssertion": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true,
|
|
||||||
"formatWithErrors": false,
|
|
||||||
"indentStyle": "space",
|
|
||||||
"indentWidth": 2,
|
|
||||||
"lineWidth": 100,
|
|
||||||
"lineEnding": "lf"
|
|
||||||
},
|
|
||||||
"javascript": {
|
|
||||||
"jsxRuntime": "transparent",
|
|
||||||
"formatter": {
|
|
||||||
"quoteStyle": "single",
|
|
||||||
"jsxQuoteStyle": "double",
|
|
||||||
"trailingCommas": "all",
|
|
||||||
"semicolons": "always",
|
|
||||||
"arrowParentheses": "always"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"css": {
|
|
||||||
"parser": {
|
|
||||||
"tailwindDirectives": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"files": {
|
|
||||||
"ignoreUnknown": false,
|
|
||||||
"includes": [
|
|
||||||
"**",
|
|
||||||
"!**/.opencode",
|
|
||||||
"!**/dist",
|
|
||||||
"!**/out",
|
|
||||||
"!**/build",
|
|
||||||
"!**/node_modules",
|
|
||||||
"!**/target",
|
|
||||||
"!**/coverage",
|
|
||||||
"!**/*.env",
|
|
||||||
"!**/*.env.*",
|
|
||||||
"!**/apps/react/src/routeTree.gen.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+33
-115
@@ -1,124 +1,42 @@
|
|||||||
# Menambahkan Aplikasi Baru ke Deployment
|
# Menambahkan Service Baru / Subdomain
|
||||||
|
|
||||||
Dokumen ini menjelaskan langkah menambahkan service baru ke `asepharyana-hub`. Root repo berfungsi sebagai hub: source aplikasi berada di `apps/<nama-app>` sebagai submodule, sedangkan Docker Compose, Traefik, dan workflow deploy tetap berada di root repo.
|
Panduan untuk menambahkan service baru di ekosistem `asepharyana/infra` (2026-08-28+, pasca monorepo).
|
||||||
|
|
||||||
## 1. Buat repo aplikasi
|
## Prinsip
|
||||||
|
|
||||||
Buat repo baru di GitHub dengan pola nama:
|
- **Aplikasi hidup di repo sendiri** (`hub`, `scraper`, `llm-api`) dengan
|
||||||
|
`flake.nix` + `.github/workflows/deploy.yml` mandiri. Repo infra TIDAK berisi kode app.
|
||||||
|
- Repo infra (`asepharyana/infra`) hanya mengatur **reverse proxy & config VPS**.
|
||||||
|
|
||||||
```text
|
## Langkah
|
||||||
https://github.com/asepharyana/asepharyana-hub-<nama-app>.git
|
|
||||||
|
1. **Buat repo aplikasi** (contoh pola: `asepharyana/scraper`).
|
||||||
|
2. **Tambahkan `flake.nix`** di repo app — derivasi Nix (lihat template di repo app yang ada:
|
||||||
|
Next.js/bun atau Rust/cargo). Nama paket = nama unit systemd.
|
||||||
|
3. **Tambahkan `.github/workflows/deploy.yml`** (pola `nix build .#<pkg>` → `nix copy ssh://`
|
||||||
|
→ `nix-env --profile /nix/var/nix/profiles/<pkg> --set` → `systemctl restart <pkg>`).
|
||||||
|
Secrets yang dibutuhkan: `SSH_PRIVATE_KEY`, `VPS_HOST`, `VPS_USER`.
|
||||||
|
4. **Di VPS**: buat user systemd + unit (mis. `/etc/systemd/system/<app>.service`,
|
||||||
|
`ExecStart=/usr/local/bin/bws-exec <app> /nix/var/nix/profiles/<app>/bin/<app>`),
|
||||||
|
pastikan app jalan di port lokal.
|
||||||
|
5. **Tambah site block** di `infra/caddy/Caddyfile.prod` (pola `import proxy <port>`),
|
||||||
|
push ke `main` → CI `caddy-deploy.yml` sync + reload + verifikasi rute.
|
||||||
|
6. **(Opsional)** Tambah unit ke `MONITORED_UNITS` di hub dashboard
|
||||||
|
(repo `asepharyana/hub`, `src/app/api/dashboard/route.ts`).
|
||||||
|
|
||||||
|
## Contoh site block Caddy
|
||||||
|
|
||||||
|
```caddyfile
|
||||||
|
nama-app.asepharyana.my.id {
|
||||||
|
import proxy <PORT>
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Lalu tambahkan ke root hub sebagai submodule:
|
## Verifikasi
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git submodule add https://github.com/asepharyana/asepharyana-hub-<nama-app>.git apps/<nama-app>
|
# Dari local
|
||||||
git submodule update --init --recursive
|
curl -s -o /dev/null -w '%{http_code}\n' https://nama-app.asepharyana.my.id/
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Tambahkan Dockerfile
|
# Dari VPS
|
||||||
|
systemctl status <app>
|
||||||
Tambahkan Dockerfile runtime di `infra/docker/<nama-app>.Dockerfile`.
|
|
||||||
|
|
||||||
Gunakan root repo sebagai build context agar Dockerfile bisa mengakses submodule path:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -f infra/docker/<nama-app>.Dockerfile -t <nama-app>:local .
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Tambahkan Compose file
|
|
||||||
|
|
||||||
Buat `infra/compose/<nama-app>.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
<nama-app>:
|
|
||||||
container_name: <nama-app>
|
|
||||||
image: ghcr.io/asepharyana/asepharyana-hub/<nama-app>:sha-<short-sha>
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
app-shared-net:
|
|
||||||
aliases:
|
|
||||||
- <nama-app>
|
|
||||||
env_file:
|
|
||||||
- ../../.env
|
|
||||||
|
|
||||||
networks:
|
|
||||||
app-shared-net:
|
|
||||||
name: app-shared-net
|
|
||||||
external: true
|
|
||||||
```
|
|
||||||
|
|
||||||
Gunakan `app-shared-net` agar service dapat diakses oleh Traefik dan service lain.
|
|
||||||
|
|
||||||
## 4. Tambahkan route Traefik
|
|
||||||
|
|
||||||
Update `infra/traefik/dynamic/apps.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
http:
|
|
||||||
routers:
|
|
||||||
<nama-app>:
|
|
||||||
rule: 'Host(`<subdomain>.asepharyana.my.id`) || Host(`<subdomain>.asepharyana.web.id`)'
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
tls: {}
|
|
||||||
middlewares:
|
|
||||||
- common-chain@file
|
|
||||||
service: <nama-app>-service
|
|
||||||
|
|
||||||
services:
|
|
||||||
<nama-app>-service:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: 'http://<nama-app>:<port>'
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Update workflow build
|
|
||||||
|
|
||||||
Update `.github/workflows/docker-build-push.yml`:
|
|
||||||
|
|
||||||
1. Tambahkan path detection untuk `apps/<nama-app>` dan `infra/docker/<nama-app>.Dockerfile`.
|
|
||||||
2. Tambahkan service ke matrix build.
|
|
||||||
3. Tambahkan mapping Dockerfile di step `Docker metadata`.
|
|
||||||
4. Tambahkan mapping compose file dan submodule path di step `Update tags and submodules`.
|
|
||||||
|
|
||||||
## 6. Update workflow deploy
|
|
||||||
|
|
||||||
Tambahkan compose file baru ke `ALL_COMPOSE_FILES` di `.github/workflows/deploy-docker.yml`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
infra/compose/<nama-app>.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7. Update dokumentasi
|
|
||||||
|
|
||||||
Update file berikut bila service baru mengubah arsitektur publik:
|
|
||||||
|
|
||||||
- `README.md`
|
|
||||||
- `ARCHITECTURE.md`
|
|
||||||
- `infra/README.md`
|
|
||||||
- `.gitmodules`
|
|
||||||
|
|
||||||
## 8. Validasi
|
|
||||||
|
|
||||||
Jalankan validasi YAML dan compose rendering:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python - <<'PY'
|
|
||||||
import pathlib, yaml
|
|
||||||
for path in pathlib.Path('infra').rglob('*.yml'):
|
|
||||||
with path.open() as fh:
|
|
||||||
yaml.safe_load(fh)
|
|
||||||
print(f'OK {path}')
|
|
||||||
for path in pathlib.Path('infra').rglob('*.yaml'):
|
|
||||||
with path.open() as fh:
|
|
||||||
yaml.safe_load(fh)
|
|
||||||
print(f'OK {path}')
|
|
||||||
PY
|
|
||||||
|
|
||||||
for f in infra/compose/*.yml; do
|
|
||||||
docker compose -f "$f" config >/dev/null && echo "OK $f"
|
|
||||||
done
|
|
||||||
```
|
|
||||||
@@ -1,52 +1,38 @@
|
|||||||
# ADR 0001: Use a Hub Repository with App Submodules
|
# ADR 0001: Infra Repo — Reverse Proxy Config Only (Submodules Removed)
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Accepted
|
**Superseded** (2026-08-28) — lihat ADR ini sebagai arsip keputusan awal.
|
||||||
|
|
||||||
## Context
|
## Context (aslinya)
|
||||||
|
|
||||||
The project contains multiple independent application services that share one deployment surface: Docker Compose, Traefik routing, GitHub Actions workflows, and operational documentation.
|
Proyek awal memakai `asepharyana-hub` sebagai monorepo: aplikasi di `apps/<service>` sebagai
|
||||||
|
git submodule, infra (compose/traefik/dokumen/CI) terpusat di root. Keputusan itu masuk akal
|
||||||
|
saat semua service berbagi satu deployment surface.
|
||||||
|
|
||||||
The services should be developed and versioned independently, while deployment infrastructure should remain centralized so production routing and compose manifests stay consistent.
|
## Decision (aslinya)
|
||||||
|
|
||||||
## Decision
|
Gunakan `asepharyana-hub` sebagai root hub: app code submodule, infra + CI di root.
|
||||||
|
|
||||||
Use `asepharyana-hub` as the root hub repository.
|
## Superseded By
|
||||||
|
|
||||||
- Application code lives under `apps/<service>` as Git submodules.
|
Mulai **2026-08-28** repo dirombak:
|
||||||
- Infrastructure lives in the root repo under `infra/`.
|
|
||||||
- Documentation lives in the root repo under `docs/`.
|
|
||||||
- CI/CD workflows live in the root repo under `.github/workflows/`.
|
|
||||||
- Root tooling stays minimal: `package.json`, Prettier, ESLint, Makefile helpers, and deployment scripts.
|
|
||||||
|
|
||||||
Current app submodules:
|
- **Parent `asepharyana-hub` → `asepharyana/infra`** — murni config reverse proxy (Caddy),
|
||||||
|
firewall, systemd drop-ins, docs. CI hanya untuk deploy Caddy.
|
||||||
| Service | Path | Remote |
|
- **App repos di-rename & self-contained**: `hub`, `scraper`, `tools`, `llm-api`.
|
||||||
| -------------- | ---------------- | --------------------------------------- |
|
Masing-masing punya `flake.nix` + `.github/workflows/deploy.yml` sendiri
|
||||||
| Elysia API | `apps/elysia` | `asepharyana/asepharyana-hub-elysia` |
|
(`nix build → nix copy → nix-env --profile → systemctl restart`).
|
||||||
| React frontend | `apps/react` | `asepharyana/asepharyana-hub-react` |
|
- **Submodule dihapus** — tidak ada lagi pointer submodule / repository_dispatch chain.
|
||||||
| Rust auth API | `apps/rust-auth` | `asepharyana/asepharyana-hub-rust-auth` |
|
- `update-submodule.yml`, `notify-parent.yml`, matrix `nix-build.yml` dihapus.
|
||||||
| Scraper API | `apps/scraper` | `asepharyana/asepharyana-hub-scraper` |
|
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
### Positive
|
### Positive
|
||||||
|
- CI tiap app independen: push ke repo app langsung build+deploy, tak perlu 2 hop.
|
||||||
- Each app can evolve in its own repository.
|
- Parent kecil & fokus: diff Caddyfile mudah di-audit.
|
||||||
- The hub pins exact submodule revisions for reproducible deployments.
|
- Tanpa submodule = tanpa `dubious ownership` / pointer drift / fetchGit pin.
|
||||||
- Deployment infrastructure remains centralized and easier to audit.
|
|
||||||
- Root tooling stays lightweight and does not impose one build system on every service.
|
|
||||||
|
|
||||||
### Negative
|
### Negative
|
||||||
|
- Koordinasi cross-repo manual (app + Caddy bila perlu port baru).
|
||||||
- Developers must understand Git submodule workflows.
|
- Repo lama `asepharyana-hub-*` redirect ke nama baru (GitHub auto).
|
||||||
- Updating a service requires updating the submodule pointer in the hub repo.
|
|
||||||
- Cross-service changes require coordinating commits across multiple repositories.
|
|
||||||
|
|
||||||
### Mitigations
|
|
||||||
|
|
||||||
- Keep `.gitmodules` accurate and minimal.
|
|
||||||
- Use `scripts/sync-submodules.sh` for local checkout consistency.
|
|
||||||
- Document service-addition steps in `docs/add-new-app.md`.
|
|
||||||
- Keep GitHub Actions responsible for Docker image builds, compose tag updates, and deployments.
|
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# ADR 0002: Production `.env` via GitHub Encrypted Secret
|
||||||
|
|
||||||
|
> **LEGACY (2026-08-28):** Repo `asepharyana-hub` sudah dirombak → `asepharyana/infra`.
|
||||||
|
> Workflow lama yang SCP `.env` ke VPS tidak dipakai lagi (deploy app pindah ke repo masing-masing,
|
||||||
|
> secrets via Bitwarden `bws-exec`). ADR ini dipertahankan sebagai arsip.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (archived)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The project runs on a remote VPS (`orangevps`, IP `45.127.35.244`) that hosts multiple services via Docker Compose. These services require environment variables (database credentials, API keys, tokens) that must not be committed to the repository.
|
||||||
|
|
||||||
|
The production `.env` file on the VPS is **not** a copy of the committed `.env` in the repo root — it contains additional secrets (Portainer tokens, Discord bot tokens, etc.) that only exist in production.
|
||||||
|
|
||||||
|
Previously, the `.env` file on the VPS was edited manually via SSH, which led to drift between the local `.env` and the production `.env`. When the database server IP or port changed in the local `.env`, the production `.env` was not updated, causing service outages.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
The production `.env` file is stored as a **GitHub Actions encrypted secret** named `ENV_FILE_PRODUCTION`. During deployment, the `.github/workflows/deploy-docker.yml` workflow writes this secret to a file and SCPs it to the VPS.
|
||||||
|
|
||||||
|
### Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
GitHub Secret (ENV_FILE_PRODUCTION)
|
||||||
|
│
|
||||||
|
▼ (deploy-docker.yml)
|
||||||
|
echo "$ENV_FILE_PRODUCTION" > .env.prod
|
||||||
|
scp .env.prod → VPS:$VPS_TARGET_DIR/.env
|
||||||
|
│
|
||||||
|
▼ (docker compose --env-file .env up)
|
||||||
|
Container reads $DATABASE_URL, $JWT_SECRET, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to update
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Read current content from the VPS
|
||||||
|
ssh root@45.127.35.244 "cat <VPS app dir, e.g. /home/code/hub>/.env"
|
||||||
|
|
||||||
|
# 2. Pipe updated content to the GitHub secret
|
||||||
|
# (requires gh CLI with repo access)
|
||||||
|
cat /path/to/updated-env | gh secret set ENV_FILE_PRODUCTION --repo asepharyana/asepharyana-hub
|
||||||
|
|
||||||
|
# 3. Trigger a redeploy to push it to the VPS
|
||||||
|
gh workflow run deploy-docker.yml
|
||||||
|
|
||||||
|
# OR apply immediately on the VPS (for hotfix):
|
||||||
|
ssh root@45.127.35.244 "sed -i 's|OLD_VALUE|NEW_VALUE|' <VPS app dir, e.g. /home/code/hub>/.env"
|
||||||
|
# Then restart affected containers
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server Topology
|
||||||
|
|
||||||
|
| Host | IP | Role |
|
||||||
|
|------|----|------|
|
||||||
|
| `orangevps` (VPS) | `45.127.35.244` | Docker host: Traefik, scraper-api, Redis, NATS, Dapr |
|
||||||
|
| `imrnes` (bare-metal) | `100.121.180.82` (Tailscale) | PostgreSQL (port 6432), Redis (port 6379), Browserless |
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
| Variable | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| `DATABASE_URL` | `postgres://asephs:hunterz@100.121.180.82:6432/hub` |
|
||||||
|
| `REDIS_URL` | `redis://redis:6379` (Docker network, overridden per-service) |
|
||||||
|
| `EXTERNAL_BROWSERLESS_WS` | `ws://43.134.105.109:3001/?token=...` (external proxy) |
|
||||||
|
|
||||||
|
> **Important:** The Docker Compose `environment:` section uses variable interpolation (`${DATABASE_URL}`), which is resolved from the `--env-file .env` at compose time — NOT from the service's `env_file`. Both must be kept in sync.
|
||||||
|
|
||||||
|
## Docker Compose Project Structure
|
||||||
|
|
||||||
|
The VPS runs a single Docker Compose project named `compose` composed of multiple files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
<VPS app dir, e.g. /home/code/hub>/infra/compose/
|
||||||
|
├── traefik.yml # Reverse proxy (TLS termination, routing)
|
||||||
|
├── shared.yml # Redis
|
||||||
|
├── nats.yml # NATS message broker + JetStream
|
||||||
|
├── dapr.yml # Dapr placement service
|
||||||
|
├── scraper.yml # Scraper API + Dapr sidecar
|
||||||
|
└── observability.yml # OTel Collector + Jaeger + Dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
All files are always included together for dependency resolution:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose \
|
||||||
|
--env-file .env \
|
||||||
|
-f infra/compose/traefik.yml \
|
||||||
|
-f infra/compose/shared.yml \
|
||||||
|
-f infra/compose/scraper.yml \
|
||||||
|
-f infra/compose/nats.yml \
|
||||||
|
-f infra/compose/dapr.yml \
|
||||||
|
-f infra/compose/observability.yml \
|
||||||
|
up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## GitHub Secrets Required
|
||||||
|
|
||||||
|
| Secret | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `SSH_PRIVATE_KEY` | SSH key for VPS access |
|
||||||
|
| `VPS_HOST` | `45.127.35.244` |
|
||||||
|
| `VPS_USER` | `root` |
|
||||||
|
| `VPS_TARGET_DIR` | `<VPS app dir, e.g. /home/code/hub>` |
|
||||||
|
| `ENV_FILE_PRODUCTION` | Full `.env` content for production |
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- Environment is version-controlled via GitHub Secrets audit log.
|
||||||
|
- No risk of committing secrets to the repo.
|
||||||
|
- Deployment is fully automated — `.env` is pushed on every deploy.
|
||||||
|
- Easy to rotate secrets: update `ENV_FILE_PRODUCTION` and redeploy.
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
|
||||||
|
- The secret is opaque — you cannot diff it or review changes via PR.
|
||||||
|
- If the secret falls out of sync with the local `.env`, services silently break on next deploy.
|
||||||
|
- Requires `gh` CLI or GitHub UI to update — not a simple file edit.
|
||||||
|
|
||||||
|
### Mitigations
|
||||||
|
|
||||||
|
- Keep the **committed `.env`** in the repo root as the source of truth for non-secret values (database URL, ports, API endpoints).
|
||||||
|
- Document any manual SSH hotfix at the same time as updating the GitHub secret.
|
||||||
|
- Run `gh secret set ENV_FILE_PRODUCTION` with the latest server `.env` content after any hotfix.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# ADR 0003: Rename Repositori & Pisahkan CI per Aplikasi
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-08-28)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Monorepo `asepharyana-hub` (app submodule + infra + CI terpusat) punya kelemahan:
|
||||||
|
- CI build+deploy semua app menyatu di parent (`nix-build.yml` matrix) — setiap push app
|
||||||
|
butuh 2 hop (notify-parent → update-submodule → nix-build), rawan drift pointer.
|
||||||
|
- Nama `asepharyana-hub` ambigu (parent & app prefix sama), dan submodule menambah kompleksitas.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Rombak total:
|
||||||
|
|
||||||
|
| Lama | Baru | Peran |
|
||||||
|
|------|------|-------|
|
||||||
|
| `asepharyana-hub` | `asepharyana/infra` | Reverse proxy (Caddy) + firewall + systemd + docs. CI: caddy-deploy saja. |
|
||||||
|
| `asepharyana-hub-hub` | `asepharyana/hub` | Portfolio SPA. CI mandiri (deploy.yml). |
|
||||||
|
| `asepharyana-hub-scraper` | `asepharyana/scraper` | Rust scraper API. CI mandiri. |
|
||||||
|
| `asepharyana-hub-tools` | `asepharyana/tools` | Tools stack (gateway/workers/frontend). CI mandiri. |
|
||||||
|
| `asepharyana-hub-llm-api` | `asepharyana/llm-api` | Rust LLM API. CI mandiri. |
|
||||||
|
| `asepharyana-hub-guide` | `asepharyana/hub-guide` | (tidak di-root; plugin guide — diarsipkan) |
|
||||||
|
|
||||||
|
Setiap app repo mendapat:
|
||||||
|
- `flake.nix` (derivasi build sendiri, tanpa fetchGit submodule)
|
||||||
|
- `.github/workflows/deploy.yml` (nix build → nix copy → nix-env --profile → systemctl restart)
|
||||||
|
- Secret `SSH_PRIVATE_KEY`, `VPS_HOST`, `VPS_USER`
|
||||||
|
|
||||||
|
Parent `infra` mendapat:
|
||||||
|
- Hapus semua submodule + `update-submodule.yml` + matrix `nix-build.yml`
|
||||||
|
- `.github/workflows/caddy-deploy.yml` (sync Caddyfile → reload → verify)
|
||||||
|
- Docs diarahkan ulang.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Positif**: CI per-app independen & cepat; parent kecil; tanpa submodule = tanpa fetchGit pin
|
||||||
|
/ dubious-ownership / pointer churn. Rename GitHub auto-redirect URL lama.
|
||||||
|
- **Negatif**: koordinasi manual bila app butuh port baru di Caddy; workflow lama di
|
||||||
|
downstream (skill/cron) perlu update referensi.
|
||||||
|
|
||||||
|
## Referensi
|
||||||
|
|
||||||
|
- `docs/add-new-app.md` — proses menambah service baru
|
||||||
|
- `infra/caddy/Caddyfile.prod` — pola site block
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Squid Proxy Configuration (Archived)
|
|
||||||
|
|
||||||
**Status**: Archived; not referenced by active infrastructure.
|
|
||||||
|
|
||||||
**Date archived**: 2026-06-04
|
|
||||||
|
|
||||||
**Original location**: `infra/squid.conf`
|
|
||||||
|
|
||||||
**Reason**: No active Docker Compose service mounts or starts Squid. The config is preserved here for reference if Squid proxying is restored later.
|
|
||||||
|
|
||||||
## Original configuration
|
|
||||||
|
|
||||||
```squid
|
|
||||||
# Allow localhost access only
|
|
||||||
acl localhost src 127.0.0.1/32 ::1/128
|
|
||||||
http_access allow localhost
|
|
||||||
|
|
||||||
# Allow Docker bridge subnets (Adjusted for our expected subnets)
|
|
||||||
acl docker_bridge src 172.16.0.0/12
|
|
||||||
http_access allow docker_bridge
|
|
||||||
|
|
||||||
# Deny all other access
|
|
||||||
http_access deny all
|
|
||||||
|
|
||||||
# Standard port - Explicitly bind to all interfaces
|
|
||||||
http_port 0.0.0.0:3128
|
|
||||||
|
|
||||||
# Hide source info
|
|
||||||
forwarded_for off
|
|
||||||
via off
|
|
||||||
```
|
|
||||||
|
|
||||||
## Restore checklist
|
|
||||||
|
|
||||||
1. Copy this config back to `infra/squid.conf`.
|
|
||||||
2. Add a Squid service to an active compose file.
|
|
||||||
3. Mount the config into the Squid container.
|
|
||||||
4. Document which services should use the proxy.
|
|
||||||
5. Validate network exposure and authentication before production use.
|
|
||||||
@@ -1,686 +0,0 @@
|
|||||||
# Moonrepo Migration Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Migrate the monorepo to moonrepo for centralized task orchestration, caching, and consistent dependency management across all 7 apps.
|
|
||||||
|
|
||||||
**Architecture:** moonrepo sits as a task orchestration layer above the existing Nix build system. `.moon/tasks/` holds shared task definitions (TypeScript and Rust). Each app gets a `moon.yml` with tags and inter-project dependencies. Nix, Docker Compose, git submodules, and infra remain untouched.
|
|
||||||
|
|
||||||
**Tech Stack:** moonrepo CLI, Node 22, Bun, TypeScript 5+, Cargo (Rust)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: Install moonrepo CLI and scaffold .moon/ directory
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `.moon/workspace.yml`
|
|
||||||
- Create: `.moon/toolchain.yml`
|
|
||||||
- Create: `.moon/tasks/` (directory)
|
|
||||||
|
|
||||||
- [ ] **Step 1: Install moonrepo CLI via curl**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://moonrepo.dev/install/moon.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Verify installation**
|
|
||||||
|
|
||||||
Run: `moon --version`
|
|
||||||
Expected: prints version number (e.g., `moon 1.x.x`)
|
|
||||||
|
|
||||||
- [ ] **Step 3: Create .moon/ scaffold directories**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p .moon/tasks
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .moon/
|
|
||||||
git commit -m "chore: scaffold .moon/ directory for moonrepo"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 2: Configure workspace.yml
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `.moon/workspace.yml`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write .moon/workspace.yml**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# https://moonrepo.dev/docs/config/workspace
|
|
||||||
$schema: "https://moonrepo.dev/schemas/workspace.json"
|
|
||||||
|
|
||||||
projects:
|
|
||||||
- "apps/*"
|
|
||||||
|
|
||||||
vcs:
|
|
||||||
manager: "git"
|
|
||||||
defaultBranch: "main"
|
|
||||||
|
|
||||||
runner:
|
|
||||||
implicitDeps:
|
|
||||||
# TypeScript apps: lint depends on build (for typecheck path)
|
|
||||||
- "typescript-build.build"
|
|
||||||
|
|
||||||
cacheTtl: 604800
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Validate config structure**
|
|
||||||
|
|
||||||
Run: `moon check`
|
|
||||||
Expected: no errors (will warn about missing project configs — expected)
|
|
||||||
|
|
||||||
- [ ] **Step 3: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .moon/workspace.yml
|
|
||||||
git commit -m "chore: configure moonrepo workspace with project glob"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 3: Configure toolchain.yml
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `.moon/toolchain.yml`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write .moon/toolchain.yml**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# https://moonrepo.dev/docs/config/toolchain
|
|
||||||
$schema: "https://moonrepo.dev/schemas/toolchain.json"
|
|
||||||
|
|
||||||
node:
|
|
||||||
version: "22.11.0"
|
|
||||||
packageManager: "bun"
|
|
||||||
bun:
|
|
||||||
version: "1.3.11"
|
|
||||||
|
|
||||||
typescript:
|
|
||||||
syncProjectReferences: true
|
|
||||||
createMissingConfig: false
|
|
||||||
routeOutDirToCache: false
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .moon/toolchain.yml
|
|
||||||
git commit -m "chore: configure moonrepo toolchain (Node 22, Bun 1.3)"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 4: Create shared TypeScript task definitions
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `.moon/tasks/typescript-build.yml`
|
|
||||||
- Create: `.moon/tasks/typescript-lint.yml`
|
|
||||||
- Create: `.moon/tasks/typescript-test.yml`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write .moon/tasks/typescript-build.yml**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# https://moonrepo.dev/docs/config/tasks
|
|
||||||
$schema: "https://moonrepo.dev/schemas/tasks.json"
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
build:
|
|
||||||
command: "bun run build"
|
|
||||||
inputs:
|
|
||||||
- "src/**/*"
|
|
||||||
- "tsconfig.json"
|
|
||||||
- "package.json"
|
|
||||||
outputs:
|
|
||||||
- ".next"
|
|
||||||
- "dist"
|
|
||||||
- ".output"
|
|
||||||
options:
|
|
||||||
cache: true
|
|
||||||
dev:
|
|
||||||
command: "bun run dev"
|
|
||||||
local: true
|
|
||||||
options:
|
|
||||||
persistent: true
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Write .moon/tasks/typescript-lint.yml**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# https://moonrepo.dev/docs/config/tasks
|
|
||||||
$schema: "https://moonrepo.dev/schemas/tasks.json"
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
lint:
|
|
||||||
command: "bun run lint"
|
|
||||||
inputs:
|
|
||||||
- "src/**/*"
|
|
||||||
- "eslint.config.mjs"
|
|
||||||
- "tsconfig.json"
|
|
||||||
options:
|
|
||||||
cache: false
|
|
||||||
typecheck:
|
|
||||||
command: "bun run check-types"
|
|
||||||
inputs:
|
|
||||||
- "src/**/*"
|
|
||||||
- "tsconfig.json"
|
|
||||||
options:
|
|
||||||
cache: false
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Write .moon/tasks/typescript-test.yml**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# https://moonrepo.dev/docs/config/tasks
|
|
||||||
$schema: "https://moonrepo.dev/schemas/tasks.json"
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
test:
|
|
||||||
command: "bun test"
|
|
||||||
inputs:
|
|
||||||
- "src/**/*"
|
|
||||||
- "test/**/*"
|
|
||||||
- "tests/**/*"
|
|
||||||
- "vitest.config.ts"
|
|
||||||
options:
|
|
||||||
cache: false
|
|
||||||
e2e:
|
|
||||||
command: "noop"
|
|
||||||
local: true
|
|
||||||
options:
|
|
||||||
cache: false
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .moon/tasks/typescript-build.yml .moon/tasks/typescript-lint.yml .moon/tasks/typescript-test.yml
|
|
||||||
git commit -m "chore: add shared TypeScript task definitions"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 5: Create shared Rust task definitions
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `.moon/tasks/rust-build.yml`
|
|
||||||
- Create: `.moon/tasks/rust-test.yml`
|
|
||||||
- Create: `.moon/tasks/rust-lint.yml`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write .moon/tasks/rust-build.yml**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# https://moonrepo.dev/docs/config/tasks
|
|
||||||
$schema: "https://moonrepo.dev/schemas/tasks.json"
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
build:
|
|
||||||
command: "cargo build --release"
|
|
||||||
platform: system
|
|
||||||
inputs:
|
|
||||||
- "src/**/*"
|
|
||||||
- "Cargo.toml"
|
|
||||||
- "Cargo.lock"
|
|
||||||
- "build.rs"
|
|
||||||
outputs:
|
|
||||||
- "target/release/*"
|
|
||||||
options:
|
|
||||||
cache: true
|
|
||||||
envFile: false
|
|
||||||
dev:
|
|
||||||
command: "cargo run"
|
|
||||||
platform: system
|
|
||||||
local: true
|
|
||||||
options:
|
|
||||||
persistent: true
|
|
||||||
envFile: false
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Write .moon/tasks/rust-test.yml**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# https://moonrepo.dev/docs/config/tasks
|
|
||||||
$schema: "https://moonrepo.dev/schemas/tasks.json"
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
test:
|
|
||||||
command: "cargo test"
|
|
||||||
platform: system
|
|
||||||
inputs:
|
|
||||||
- "src/**/*"
|
|
||||||
- "Cargo.toml"
|
|
||||||
- "Cargo.lock"
|
|
||||||
- "tests/**/*"
|
|
||||||
options:
|
|
||||||
cache: false
|
|
||||||
envFile: false
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Write .moon/tasks/rust-lint.yml**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# https://moonrepo.dev/docs/config/tasks
|
|
||||||
$schema: "https://moonrepo.dev/schemas/tasks.json"
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
lint:
|
|
||||||
command: "cargo clippy -- -D warnings"
|
|
||||||
platform: system
|
|
||||||
inputs:
|
|
||||||
- "src/**/*"
|
|
||||||
- "Cargo.toml"
|
|
||||||
options:
|
|
||||||
cache: false
|
|
||||||
envFile: false
|
|
||||||
fmt-check:
|
|
||||||
command: "cargo fmt --check"
|
|
||||||
platform: system
|
|
||||||
inputs:
|
|
||||||
- "src/**/*"
|
|
||||||
options:
|
|
||||||
cache: false
|
|
||||||
envFile: false
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .moon/tasks/rust-build.yml .moon/tasks/rust-test.yml .moon/tasks/rust-lint.yml
|
|
||||||
git commit -m "chore: add shared Rust task definitions"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 6: Create per-app moon.yml for TypeScript apps
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `apps/nextjs/moon.yml`
|
|
||||||
- Create: `apps/elysia/moon.yml`
|
|
||||||
- Create: `apps/solidjs/moon.yml`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write apps/nextjs/moon.yml**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# https://moonrepo.dev/docs/config/project
|
|
||||||
$schema: "https://moonrepo.dev/schemas/project.json"
|
|
||||||
|
|
||||||
type: "application"
|
|
||||||
language: "typescript"
|
|
||||||
platform: "node"
|
|
||||||
tags:
|
|
||||||
- "lang:typescript"
|
|
||||||
- "type:frontend"
|
|
||||||
|
|
||||||
dependsOn:
|
|
||||||
- id: "rust-auth"
|
|
||||||
- id: "elysia"
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Write apps/elysia/moon.yml**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# https://moonrepo.dev/docs/config/project
|
|
||||||
$schema: "https://moonrepo.dev/schemas/project.json"
|
|
||||||
|
|
||||||
type: "application"
|
|
||||||
language: "typescript"
|
|
||||||
platform: "bun"
|
|
||||||
tags:
|
|
||||||
- "lang:typescript"
|
|
||||||
- "type:backend"
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Write apps/solidjs/moon.yml**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# https://moonrepo.dev/docs/config/project
|
|
||||||
$schema: "https://moonrepo.dev/schemas/project.json"
|
|
||||||
|
|
||||||
type: "application"
|
|
||||||
language: "typescript"
|
|
||||||
platform: "bun"
|
|
||||||
tags:
|
|
||||||
- "lang:typescript"
|
|
||||||
- "type:frontend"
|
|
||||||
|
|
||||||
dependsOn:
|
|
||||||
- id: "elysia"
|
|
||||||
- id: "rust-auth"
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add apps/nextjs/moon.yml apps/elysia/moon.yml apps/solidjs/moon.yml
|
|
||||||
git commit -m "chore: add moon.yml for TypeScript apps (nextjs, elysia, solidjs)"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 7: Create per-app moon.yml for Rust apps
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `apps/rust/moon.yml`
|
|
||||||
- Create: `apps/rust-auth/moon.yml`
|
|
||||||
- Create: `apps/leptos/moon.yml`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write apps/rust/moon.yml**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# https://moonrepo.dev/docs/config/project
|
|
||||||
$schema: "https://moonrepo.dev/schemas/project.json"
|
|
||||||
|
|
||||||
type: "application"
|
|
||||||
language: "rust"
|
|
||||||
platform: "system"
|
|
||||||
tags:
|
|
||||||
- "lang:rust"
|
|
||||||
- "type:backend"
|
|
||||||
|
|
||||||
fileGroups:
|
|
||||||
sources:
|
|
||||||
- "src/**/*.rs"
|
|
||||||
- "Cargo.toml"
|
|
||||||
- "Cargo.lock"
|
|
||||||
- "build.rs"
|
|
||||||
- "rustfmt.toml"
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Write apps/rust-auth/moon.yml**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# https://moonrepo.dev/docs/config/project
|
|
||||||
$schema: "https://moonrepo.dev/schemas/project.json"
|
|
||||||
|
|
||||||
type: "application"
|
|
||||||
language: "rust"
|
|
||||||
platform: "system"
|
|
||||||
tags:
|
|
||||||
- "lang:rust"
|
|
||||||
- "type:backend"
|
|
||||||
|
|
||||||
fileGroups:
|
|
||||||
sources:
|
|
||||||
- "src/**/*.rs"
|
|
||||||
- "Cargo.toml"
|
|
||||||
- "Cargo.lock"
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Write apps/leptos/moon.yml**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# https://moonrepo.dev/docs/config/project
|
|
||||||
$schema: "https://moonrepo.dev/schemas/project.json"
|
|
||||||
|
|
||||||
type: "application"
|
|
||||||
language: "rust"
|
|
||||||
platform: "system"
|
|
||||||
tags:
|
|
||||||
- "lang:rust"
|
|
||||||
- "type:frontend"
|
|
||||||
|
|
||||||
dependsOn:
|
|
||||||
- id: "rust"
|
|
||||||
|
|
||||||
fileGroups:
|
|
||||||
sources:
|
|
||||||
- "src/**/*.rs"
|
|
||||||
- "Cargo.toml"
|
|
||||||
- "Cargo.lock"
|
|
||||||
- "Trunk.toml"
|
|
||||||
- "rust-toolchain.toml"
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add apps/rust/moon.yml apps/rust-auth/moon.yml apps/leptos/moon.yml
|
|
||||||
git commit -m "chore: add moon.yml for Rust apps (rust, rust-auth, leptos)"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 8: Create moon.yml for 9router
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `apps/9router/moon.yml`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write apps/9router/moon.yml**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# https://moonrepo.dev/docs/config/project
|
|
||||||
$schema: "https://moonrepo.dev/schemas/project.json"
|
|
||||||
|
|
||||||
type: "application"
|
|
||||||
language: "unknown"
|
|
||||||
platform: "system"
|
|
||||||
tags:
|
|
||||||
- "type:router"
|
|
||||||
|
|
||||||
fileGroups:
|
|
||||||
sources:
|
|
||||||
- "src/**/*"
|
|
||||||
- "next.config.mjs"
|
|
||||||
- "package.json"
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add apps/9router/moon.yml
|
|
||||||
git commit -m "chore: add moon.yml for 9router"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 9: Update flake.nix — add moon CLI to devShell
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `flake.nix`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Add moon to nativeBuildInputs in flake.nix**
|
|
||||||
|
|
||||||
Find the `devShells.default` block in `flake.nix`. Add `moon` to `nativeBuildInputs`:
|
|
||||||
|
|
||||||
```
|
|
||||||
devShells.default = pkgs.mkShell {
|
|
||||||
name = "ultimate-asepharyana-dev";
|
|
||||||
nativeBuildInputs = with pkgs; [
|
|
||||||
rustToolchain
|
|
||||||
bun
|
|
||||||
nodejs_22
|
|
||||||
pkg-config
|
|
||||||
openssl
|
|
||||||
trunk
|
|
||||||
wasm-bindgen-cli
|
|
||||||
binaryen
|
|
||||||
process-compose
|
|
||||||
mysql84
|
|
||||||
redis
|
|
||||||
minio-client
|
|
||||||
gh
|
|
||||||
git
|
|
||||||
moon # <-- add this line
|
|
||||||
];
|
|
||||||
# ... shellHook unchanged
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Verify Nix can still evaluate the flake**
|
|
||||||
|
|
||||||
Run: `nix flake check --no-build 2>&1 | head -20`
|
|
||||||
Expected: no evaluation errors
|
|
||||||
|
|
||||||
- [ ] **Step 3: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add flake.nix
|
|
||||||
git commit -m "chore: add moon CLI to Nix devShell"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 10: Update .gitignore
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `.gitignore`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Add moonrepo cache entries to .gitignore**
|
|
||||||
|
|
||||||
Append to `.gitignore`:
|
|
||||||
|
|
||||||
```gitignore
|
|
||||||
# moonrepo
|
|
||||||
.moon/cache
|
|
||||||
.~moon
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .gitignore
|
|
||||||
git commit -m "chore: add moonrepo cache entries to .gitignore"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 11: Validate installation with moon check
|
|
||||||
|
|
||||||
**Files:** (none — validation only)
|
|
||||||
|
|
||||||
- [ ] **Step 1: Run moon check**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
moon check
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: `OK` or zero errors. If warnings about unresolved project IDs appear, verify that `apps/*` glob in `workspace.yml` matches all project directories.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Run moon query projects**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
moon query projects
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: lists all 7 projects with their tags: nextjs, elysia, solidjs, rust, rust-auth, leptos, 9router
|
|
||||||
|
|
||||||
- [ ] **Step 3: Verify tag queries work**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
moon query projects --tag lang:typescript
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: nextjs, elysia, solidjs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
moon query projects --tag lang:rust
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: rust, rust-auth, leptos
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit (if any fixes were needed)**
|
|
||||||
|
|
||||||
No commit needed if check passes clean.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 12: Test — moon run build on TypeScript apps
|
|
||||||
|
|
||||||
**Files:** (none — test only)
|
|
||||||
|
|
||||||
- [ ] **Step 1: Build nextjs**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
moon run nextjs:build
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: `next build` runs successfully, outputs to `.next/`
|
|
||||||
|
|
||||||
- [ ] **Step 2: Build elysia**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
moon run elysia:build
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: `bun build` runs successfully, outputs to `dist/`
|
|
||||||
|
|
||||||
- [ ] **Step 3: Build solidjs**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
moon run solidjs:build
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: `vinxi build` runs successfully, outputs to `.output/`
|
|
||||||
|
|
||||||
- [ ] **Step 4: Verify caching on second build (nextjs)**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
moon run nextjs:build
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: `Cached` — no rebuild, uses moonrepo cache
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 13: Test — moon run lint and test
|
|
||||||
|
|
||||||
**Files:** (none — test only)
|
|
||||||
|
|
||||||
- [ ] **Step 1: Run lint across TypeScript apps**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
moon run :lint
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: all apps with a `lint` task run it. Note failures as they exist pre-migration (not caused by moonrepo).
|
|
||||||
|
|
||||||
- [ ] **Step 2: Run tests across TypeScript apps**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
moon run :test
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: all apps with a `test` task run it.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Run tag-scoped commands**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
moon run --tag lang:typescript :build
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: builds nextjs, elysia, solidjs only (not Rust apps).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 14: Final commit and documentation
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `.gitignore` (if any final updates)
|
|
||||||
|
|
||||||
- [ ] **Step 1: Final moon check**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
moon check
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: clean, no errors.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Commit any remaining changes**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git status
|
|
||||||
```
|
|
||||||
|
|
||||||
If nothing outstanding, move on.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Verify the full workspace is clean**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git status
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: working tree clean.
|
|
||||||
@@ -1,712 +0,0 @@
|
|||||||
# PostgreSQL Migration Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Migrate elysia (Drizzle ORM + MySQL) and rust (SeaORM + MySQL) apps to PostgreSQL via direct cutover.
|
|
||||||
|
|
||||||
**Architecture:** Export MySQL data, convert schema to PostgreSQL format, import into target PostgreSQL instance, update app drivers and connection strings, deploy and validate.
|
|
||||||
|
|
||||||
**Tech Stack:** MySQL (source), PostgreSQL (target), Drizzle ORM (elysia), SeaORM (rust), pgloader or manual conversion for schema migration.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
### Elysia App Changes
|
|
||||||
- `apps/elysia/src/db/lib/database.ts` — Replace mysql2 driver with postgres driver
|
|
||||||
- `apps/elysia/src/db/lib/schema.ts` — Replace mysqlTable with pgTable, update column types
|
|
||||||
- `apps/elysia/package.json` — Replace mysql2 with pg dependency
|
|
||||||
- `.env` or config file — Update DATABASE_URL to PostgreSQL connection string
|
|
||||||
|
|
||||||
### Rust App Changes
|
|
||||||
- `apps/rust/Cargo.toml` — Replace sqlx-mysql feature with sqlx-postgres
|
|
||||||
- `apps/rust/src/infra/db_setup.rs` — Update DbBackend::MySql to DbBackend::Postgres, adjust SQL syntax
|
|
||||||
- Config/environment — Update DATABASE_URL to PostgreSQL connection string
|
|
||||||
|
|
||||||
### Migration Artifacts
|
|
||||||
- `mysql_backup.sql` — MySQL dump (created during migration, not committed)
|
|
||||||
- `converted.sql` — PostgreSQL-compatible dump (created during migration, not committed)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task Breakdown
|
|
||||||
|
|
||||||
### Task 1: Backup MySQL and Export Schema
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `mysql_backup.sql` (temporary, not committed)
|
|
||||||
|
|
||||||
- [ ] **Step 1: Export MySQL database**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /mnt/code/bp3/ultimate-asepharyana.tech
|
|
||||||
mysqldump -u <mysql_user> -p <mysql_password> -h <mysql_host> <database_name> > mysql_backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: File created with full schema + data. Verify file size > 1MB (contains data).
|
|
||||||
|
|
||||||
-[]**Step 2: Verify backup integrity**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check row counts in backup
|
|
||||||
grep "INSERT INTO" mysql_backup.sql | wc -l
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Multiple INSERT statements present. Note row counts for later validation.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Document backup location**
|
|
||||||
|
|
||||||
Store `mysql_backup.sql` in safe location (not in git). This is rollback insurance.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 2: Convert MySQL Schema to PostgreSQL
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `converted.sql` (temporary, not committed)
|
|
||||||
|
|
||||||
- [ ] **Step 1: Install pgloader (if not present)**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# macOS
|
|
||||||
brew install pgloader
|
|
||||||
|
|
||||||
# Linux (Ubuntu/Debian)
|
|
||||||
sudo apt-get install pgloader
|
|
||||||
|
|
||||||
# Or use Docker
|
|
||||||
docker run --rm -v $(pwd):/data pgloader/pgloader pgloader /data/mysql_backup.sql postgresql://asephs:hunterz@100.108.1.124:5432/hub
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Convert MySQL dump to PostgreSQL**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pgloader mysql_backup.sql postgresql://asephs:hunterz@100.108.1.124:5432/hub
|
|
||||||
```
|
|
||||||
|
|
||||||
Or manually convert if pgloader unavailable:
|
|
||||||
- Replace `AUTO_INCREMENT` with `SERIAL` or `BIGSERIAL`
|
|
||||||
- Replace `DATETIME` with `TIMESTAMP`
|
|
||||||
- Replace backticks with double quotes
|
|
||||||
- Update index syntax for PostgreSQL
|
|
||||||
|
|
||||||
Expected: Conversion completes without errors. Check for warnings about type conversions.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Verify conversion output**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# If using pgloader, it creates converted.sql automatically
|
|
||||||
# If manual, save converted schema to file
|
|
||||||
cat converted.sql | head -50
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: PostgreSQL-compatible SQL syntax (no backticks, SERIAL types, TIMESTAMP).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 3: Test PostgreSQL Import
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Target: PostgreSQL instance at `postgresql://asephs:hunterz@100.108.1.124:5432/hub`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Connect to target PostgreSQL**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql postgresql://asephs:hunterz@100.108.1.124:5432/hub
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Connected to PostgreSQL. Prompt shows `hub=#`.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Import converted schema**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql postgresql://asephs:hunterz@100.108.1.124:5432/hub < converted.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Import completes. Check for errors (should be none).
|
|
||||||
|
|
||||||
- [ ] **Step 3: Validate table creation**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "\dt"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: All tables listed (User, Account, Session, Role, Permission, UserRole, ImageCache, etc.).
|
|
||||||
|
|
||||||
- []**Step 4: Validate row counts**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "SELECT COUNT(*) FROM \"User\";"
|
|
||||||
psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "SELECT COUNT(*) FROM \"Account\";"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Row counts match MySQL backup (from Task 1, Step 2).
|
|
||||||
|
|
||||||
- [ ] **Step 5: Validate indexes**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "\di"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: All indexes present (email_idx, username_idx, userId_idx, sessionToken_idx, etc.).
|
|
||||||
|
|
||||||
- [ ] **Step 6: Validate foreign keys**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "SELECT constraint_name, table_name FROM information_schema.table_constraints WHERE constraint_type = 'FOREIGN KEY';"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Foreign key constraints listed (User→Account, User→Session, etc.).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 4: Update Elysia Database Driver
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `apps/elysia/src/db/lib/database.ts`
|
|
||||||
- Modify: `apps/elysia/src/db/lib/schema.ts`
|
|
||||||
- Modify: `apps/elysia/package.json`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Update package.json dependencies**
|
|
||||||
|
|
||||||
Replace mysql2 with pg:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"drizzle-orm": "^0.45.2",
|
|
||||||
"pg": "^8.11.0",
|
|
||||||
"elysia": "^1.4.28"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"drizzle-kit": "^0.31.10"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Run: `cd apps/elysia && bun install`
|
|
||||||
|
|
||||||
Expected: pg installed, mysql2 removed from node_modules.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Update database.ts driver**
|
|
||||||
|
|
||||||
Replace entire file:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
|
||||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
||||||
import postgres from 'postgres'
|
|
||||||
import * as schema from './schema'
|
|
||||||
|
|
||||||
export type Database = PostgresJsDatabase<typeof schema>
|
|
||||||
|
|
||||||
let dbInstance: Database | null = null
|
|
||||||
let sqlInstance: ReturnType<typeof postgres> | null = null
|
|
||||||
|
|
||||||
export function initializeDb(databaseUrl: string): Database {
|
|
||||||
if (dbInstance) {
|
|
||||||
return dbInstance
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlInstance = postgres(databaseUrl)
|
|
||||||
dbInstance = drizzle(sqlInstance, { schema, mode: 'default' })
|
|
||||||
|
|
||||||
return dbInstance
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDb(): Database {
|
|
||||||
if (!dbInstance) {
|
|
||||||
throw new Error('Database not initialized. Call initializeDb first.')
|
|
||||||
}
|
|
||||||
return dbInstance
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function closeDb() {
|
|
||||||
if (sqlInstance) {
|
|
||||||
await sqlInstance.end()
|
|
||||||
sqlInstance = null
|
|
||||||
dbInstance = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: File updated. No syntax errors.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Update schema.ts imports**
|
|
||||||
|
|
||||||
Replace:
|
|
||||||
```typescript
|
|
||||||
import {
|
|
||||||
index,
|
|
||||||
int,
|
|
||||||
mysqlTable,
|
|
||||||
primaryKey,
|
|
||||||
text,
|
|
||||||
timestamp,
|
|
||||||
varchar,
|
|
||||||
} from 'drizzle-orm/mysql-core'
|
|
||||||
```
|
|
||||||
|
|
||||||
With:
|
|
||||||
```typescript
|
|
||||||
import {
|
|
||||||
index,
|
|
||||||
integer,
|
|
||||||
pgTable,
|
|
||||||
primaryKey,
|
|
||||||
text,
|
|
||||||
timestamp,
|
|
||||||
varchar,
|
|
||||||
} from 'drizzle-orm/postgres-core'
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Update schema.ts table definitions**
|
|
||||||
|
|
||||||
Replace all `mysqlTable` with `pgTable` and `int` with `integer`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Before
|
|
||||||
export const users = mysqlTable(
|
|
||||||
'User',
|
|
||||||
{
|
|
||||||
id: varchar('id', { length: 255 }).primaryKey(),
|
|
||||||
name: varchar('name', { length: 255 }),
|
|
||||||
// ...
|
|
||||||
},
|
|
||||||
// ...
|
|
||||||
)
|
|
||||||
|
|
||||||
// After
|
|
||||||
export const users = pgTable(
|
|
||||||
'User',
|
|
||||||
{
|
|
||||||
id: varchar('id', { length: 255 }).primaryKey(),
|
|
||||||
name: varchar('name', { length: 255 }),
|
|
||||||
// ...
|
|
||||||
},
|
|
||||||
// ...
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Do this for all tables: users, accounts, sessions, roles, permissions, userRoles, and any others.
|
|
||||||
|
|
||||||
Expected: All `mysqlTable` → `pgTable`, all `int` → `integer`.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Update environment variable**
|
|
||||||
|
|
||||||
Set DATABASE_URL in `.env` or deployment config:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DATABASE_URL=postgresql://asephs:hunterz@100.108.1.124:5432/hub
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Environment variable set and accessible to elysia app.
|
|
||||||
|
|
||||||
- [ ] **Step 6: Test elysia connection**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/elysia
|
|
||||||
bun run src/index.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: App starts without connection errors. Check logs for "Database initialized" or similar.
|
|
||||||
|
|
||||||
- [ ] **Step 7: Commit elysia changes**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /mnt/code/bp3/ultimate-asepharyana.tech
|
|
||||||
git add apps/elysia/src/db/lib/database.ts apps/elysia/src/db/lib/schema.ts apps/elysia/package.json
|
|
||||||
git commit -m "feat(elysia): migrate database driver from MySQL to PostgreSQL"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Commit created with message.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 5: Update Rust Database Driver
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `apps/rust/Cargo.toml`
|
|
||||||
- Modify: `apps/rust/src/infra/db_setup.rs`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Update Cargo.toml features**
|
|
||||||
|
|
||||||
Replace:
|
|
||||||
```toml
|
|
||||||
sea-orm = { version = "1.1.19", features = ["sqlx-mysql", "runtime-tokio-rustls", "macros", "with-chrono", "with-uuid"] }
|
|
||||||
```
|
|
||||||
|
|
||||||
With:
|
|
||||||
```toml
|
|
||||||
sea-orm = { version = "1.1.19", features = ["sqlx-postgres", "runtime-tokio-rustls", "macros", "with-chrono", "with-uuid"] }
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Cargo.toml updated. Feature changed from sqlx-mysql to sqlx-postgres.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Update db_setup.rs backend check**
|
|
||||||
|
|
||||||
Replace:
|
|
||||||
```rust
|
|
||||||
match backend {
|
|
||||||
DbBackend::MySql => {
|
|
||||||
// MySQL-specific logic
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
info!("ℹ️ Skipping schema init for non-MySQL backend");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
With:
|
|
||||||
```rust
|
|
||||||
match backend {
|
|
||||||
DbBackend::Postgres => {
|
|
||||||
// PostgreSQL-specific logic
|
|
||||||
let tables = vec![(
|
|
||||||
"ImageCache",
|
|
||||||
schema
|
|
||||||
.create_table_from_entity(image_cache::Entity)
|
|
||||||
.if_not_exists()
|
|
||||||
.to_owned(),
|
|
||||||
)];
|
|
||||||
|
|
||||||
for (name, stmt) in tables {
|
|
||||||
match db.execute(backend.build(&stmt)).await {
|
|
||||||
Ok(_) => info!(" ✓ Table '{}' checked/created", name),
|
|
||||||
Err(e) => {
|
|
||||||
error!(" [!] Failed to create table '{}': {}", name, e);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostgreSQL index creation (different syntax)
|
|
||||||
let index_sql = "CREATE INDEX IF NOT EXISTS idx_image_cache_cdn_url ON \"ImageCache\" (cdn_url)";
|
|
||||||
match db.execute(Statement::from_string(backend, index_sql)).await {
|
|
||||||
Ok(_) => info!(" ✓ Index 'idx_image_cache_cdn_url' ensured"),
|
|
||||||
Err(e) => {
|
|
||||||
let err_str = e.to_string();
|
|
||||||
// PostgreSQL duplicate index error
|
|
||||||
if err_str.contains("already exists") {
|
|
||||||
info!(" ✓ Index 'idx_image_cache_cdn_url' already exists");
|
|
||||||
} else {
|
|
||||||
error!(" [!] Failed to create index on ImageCache: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("✅ Database schema initialization complete.");
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
info!("ℹ️ Skipping schema init for non-PostgreSQL backend");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: db_setup.rs updated with PostgreSQL backend handling.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Update environment variable**
|
|
||||||
|
|
||||||
Set DATABASE_URL in `.env` or deployment config:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DATABASE_URL=postgresql://asephs:hunterz@100.108.1.124:5432/hub
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Environment variable set and accessible to rust app.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Rebuild rust app**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/rust
|
|
||||||
cargo build --release
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Build completes without errors. Compilation uses sqlx-postgres feature.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Test rust connection**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/rust
|
|
||||||
cargo run
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: App starts without connection errors. Check logs for "Database schema initialization complete" or similar.
|
|
||||||
|
|
||||||
- [ ] **Step 6: Commit rust changes**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /mnt/code/bp3/ultimate-asepharyana.tech
|
|
||||||
git add apps/rust/Cargo.toml apps/rust/src/infra/db_setup.rs
|
|
||||||
git commit -m "feat(rust): migrate database driver from MySQL to PostgreSQL"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Commit created with message.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 6: Smoke Tests - Elysia App
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Test: Manual testing via HTTP requests or app UI
|
|
||||||
|
|
||||||
- [ ] **Step 1: Start elysia app**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/elysia
|
|
||||||
bun run src/index.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: App running on configured port (check logs for port).
|
|
||||||
|
|
||||||
- [ ] **Step 2: Test user login**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:3000/auth/login \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"email":"test@example.com","password":"password"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Response 200 or 401 (auth error is OK, connection error is not).
|
|
||||||
|
|
||||||
-[]**Step 3: Test user creation (if endpoint exists)**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:3000/users \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"name":"Test User","email":"newuser@example.com"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Response 200/201 or 400 (validation error is OK).
|
|
||||||
|
|
||||||
- [ ] **Step 4: Test session retrieval**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X GET http://localhost:3000/sessions \
|
|
||||||
-H "Authorization: Bearer <token>"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Response 200 with session data or 401 (auth error is OK).
|
|
||||||
|
|
||||||
- [ ] **Step 5: Check database logs**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# In elysia app logs, verify queries are executing against PostgreSQL
|
|
||||||
# Look for connection strings or query logs showing PostgreSQL
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Logs show PostgreSQL queries (not MySQL).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 7: Smoke Tests - Rust App
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Test: Manual testing via HTTP requests or app UI
|
|
||||||
|
|
||||||
- [ ] **Step 1: Start rust app**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/rust
|
|
||||||
cargo run --release
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: App running on configured port (check logs for port).
|
|
||||||
|
|
||||||
- [ ] **Step 2: Test image cache endpoint (if exists)**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X GET http://localhost:8000/api/cache/status
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Response 200 with cache status or 404 (endpoint may not exist).
|
|
||||||
|
|
||||||
- [ ] **Step 3: Test scraping/CDN endpoint**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X GET http://localhost:8000/api/health
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Response 200 with health status.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Check database logs**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# In rust app logs, verify queries are executing against PostgreSQL
|
|
||||||
# Look for connection strings or query logs showing PostgreSQL
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Logs show PostgreSQL queries (not MySQL).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 8: Data Integrity Validation
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Test: PostgreSQL queries
|
|
||||||
|
|
||||||
- [] **Step 1: Verify user count**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "SELECT COUNT(*) as user_count FROM \"User\";"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Count matches MySQL backup count (from Task 1, Step 2).
|
|
||||||
|
|
||||||
- [ ] **Step 2: Verify account count**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "SELECT COUNT(*) as account_count FROM \"Account\";"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Count matches MySQL backup.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Verify session count**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "SELECT COUNT(*) as session_count FROM \"Session\";"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Count matches MySQL backup.
|
|
||||||
|
|
||||||
- [] **Step 4: Verify no orphaned foreign keys**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "
|
|
||||||
SELECT a.id FROM \"Account\" a
|
|
||||||
LEFT JOIN \"User\" u ON a.user_id = u.id
|
|
||||||
WHERE u.id IS NULL;
|
|
||||||
"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: No rows returned (no orphaned accounts).
|
|
||||||
|
|
||||||
- [ ] **Step 5: Verify role/permission relationships**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "SELECT COUNT(*) as role_count FROM \"Role\";"
|
|
||||||
psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "SELECT COUNT(*) as permission_count FROM \"Permission\";"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Counts match MySQL backup.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 9: Performance Baseline
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Test: Query performance comparison
|
|
||||||
|
|
||||||
- [ ] **Step 1: Benchmark user query on PostgreSQL**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "EXPLAIN ANALYZE SELECT * FROM \"User\" WHERE email = 'test@example.com';"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Query plan shows index usage (Seq Scan or Index Scan). Note execution time.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Benchmark account query on PostgreSQL**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "EXPLAIN ANALYZE SELECT * FROM \"Account\" WHERE user_id = 'user-123';"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Query plan shows index usage. Note execution time.
|
|
||||||
|
|
||||||
- [ ] **Step 3: Compare with MySQL baseline (if available)**
|
|
||||||
|
|
||||||
If MySQL is still running, run same queries and compare execution times.
|
|
||||||
|
|
||||||
Expected: PostgreSQL performance similar or better than MySQL.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 10: Cleanup and Documentation
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `MIGRATION_LOG.md` (optional, for documentation)
|
|
||||||
|
|
||||||
- [ ] **Step 1: Remove temporary files**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rm mysql_backup.sql converted.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Temporary migration files deleted.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Document migration completion**
|
|
||||||
|
|
||||||
Create `MIGRATION_LOG.md`:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# PostgreSQL Migration Log
|
|
||||||
|
|
||||||
**Date:** 2026-05-25
|
|
||||||
**Status:** ✅ Complete
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
- Migrated elysia app from MySQL to PostgreSQL
|
|
||||||
- Migrated rust app from MySQL to PostgreSQL
|
|
||||||
- All data validated and integrity confirmed
|
|
||||||
- Apps tested and operational
|
|
||||||
|
|
||||||
## Changes
|
|
||||||
- elysia: Updated database driver (mysql2 → postgres), schema (mysqlTable → pgTable)
|
|
||||||
- rust: Updated Cargo.toml feature (sqlx-mysql → sqlx-postgres), db_setup.rs backend handling
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
- Row counts match pre-migration
|
|
||||||
- Foreign keys intact
|
|
||||||
- Indexes present and performant
|
|
||||||
- Auth flow functional
|
|
||||||
- Session management working
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
MySQL backup available at: [location if kept]
|
|
||||||
To rollback: Restore MySQL from backup, revert connection strings, redeploy apps.
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Final commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add MIGRATION_LOG.md
|
|
||||||
git commit -m "docs: add PostgreSQL migration completion log"
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Commit created.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Verify all apps running**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check elysia
|
|
||||||
curl http://localhost:3000/health
|
|
||||||
|
|
||||||
# Check rust
|
|
||||||
curl http://localhost:8000/health
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Both apps respond with 200 status.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Self-Review
|
|
||||||
|
|
||||||
**Spec Coverage:**
|
|
||||||
- ✅ Pre-migration (backup, convert, test) — Tasks 1-3
|
|
||||||
- ✅ Elysia code updates (driver, schema, env) — Task 4
|
|
||||||
- ✅ Rust code updates (Cargo.toml, db_setup.rs, env) — Task 5
|
|
||||||
- ✅ Smoke tests (auth, endpoints, logs) — Tasks 6-7
|
|
||||||
- ✅ Data validation (row counts, foreign keys) — Task 8
|
|
||||||
- ✅ Performance baseline — Task 9
|
|
||||||
- ✅ Cleanup and documentation — Task 10
|
|
||||||
|
|
||||||
**Placeholder Scan:**
|
|
||||||
- ✅ No TBD/TODO
|
|
||||||
- ✅ All code blocks complete
|
|
||||||
- ✅ All commands exact with expected output
|
|
||||||
- ✅ All file paths exact
|
|
||||||
|
|
||||||
**Type Consistency:**
|
|
||||||
- ✅ Database type: `PostgresJsDatabase` (elysia), `DbBackend::Postgres` (rust)
|
|
||||||
- ✅ Connection string format consistent: `postgresql://asephs:hunterz@100.108.1.124:5432/hub`
|
|
||||||
- ✅ Table names consistent: "User", "Account", "Session", etc.
|
|
||||||
@@ -1,533 +0,0 @@
|
|||||||
# Production GitHub Actions Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Rework GitHub Actions build/deploy workflows into a production baseline with reliable React submodule updates, least-privilege permissions, clear deploy behavior, and current official action versions.
|
|
||||||
|
|
||||||
**Architecture:** Keep two workflows: `docker-build-push.yml` for detect/build/manifest updates, and `deploy-docker.yml` for VPS deployment. Add dispatch submodule SHA readiness checks before parent pointer updates, and remove recursive submodule checkout from deploy runner.
|
|
||||||
|
|
||||||
**Tech Stack:** GitHub Actions YAML, GitHub-hosted Ubuntu runners, Docker Buildx, GHCR, git submodules, Docker Compose over SSH.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
- Modify `.github/workflows/docker-build-push.yml`: add default permissions, validate repository dispatch payloads, wait for submodule SHAs, keep selective matrix builds, harden manifest update.
|
|
||||||
- Modify `.github/workflows/deploy-docker.yml`: add default permissions, remove recursive checkout, keep auto deploy from successful build, make deploy logs clearer.
|
|
||||||
- Modify `.github/dependabot.yml`: add GitHub Actions update config so official actions stay current.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: Harden build workflow permissions and dispatch validation
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `.github/workflows/docker-build-push.yml`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Add workflow-level read permissions**
|
|
||||||
|
|
||||||
At top level, after `concurrency`, add:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected shape:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: ghcr.io
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Replace dispatch parser with payload validation**
|
|
||||||
|
|
||||||
In `.github/workflows/docker-build-push.yml`, replace `Parse repository_dispatch payload` step body with:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- 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
|
|
||||||
rust-api|elysia-api|react-web|9router) ;;
|
|
||||||
*)
|
|
||||||
echo "::error::Unsupported service '$SERVICE'. Expected one of: rust-api, elysia-api, react-web, 9router"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$SHA" in
|
|
||||||
*[!0-9a-fA-F]*|???????????????????????????????????????|?????????????????????????????????????????*)
|
|
||||||
echo "::error::Invalid sha '$SHA'. Expected 40 hex characters"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
declare -a SERVICES=("rust-api" "elysia-api" "react-web" "9router")
|
|
||||||
for svc in "${SERVICES[@]}"; do
|
|
||||||
if [ "$SERVICE" = "$svc" ]; then
|
|
||||||
echo "${svc}=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "${svc}=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Run YAML syntax check**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python - <<'PY'
|
|
||||||
from pathlib import Path
|
|
||||||
import yaml
|
|
||||||
for path in Path('.github/workflows').glob('*.yml'):
|
|
||||||
yaml.safe_load(path.read_text())
|
|
||||||
print(f'OK {path}')
|
|
||||||
PY
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected:
|
|
||||||
|
|
||||||
```text
|
|
||||||
OK .github/workflows/deploy-docker.yml
|
|
||||||
OK .github/workflows/docker-build-push.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .github/workflows/docker-build-push.yml
|
|
||||||
git commit -m "ci: validate dispatch payloads"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 2: Add submodule SHA readiness wait before build/update
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `.github/workflows/docker-build-push.yml`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Add readiness job after changes job**
|
|
||||||
|
|
||||||
Insert this job between `changes` and `build`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
wait-submodule-ref:
|
|
||||||
needs: [changes]
|
|
||||||
if: github.event_name == 'repository_dispatch'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
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
|
|
||||||
"rust-api") REPO="https://github.com/MythEclipse/ultimate-asepharyana-tech-rust.git" ;;
|
|
||||||
"elysia-api") REPO="https://github.com/MythEclipse/ultimate-asepharyana-tech-elysia.git" ;;
|
|
||||||
"react-web") REPO="https://github.com/MythEclipse/ultimate-asepharyana-tech-react.git" ;;
|
|
||||||
"9router") REPO="https://github.com/MythEclipse/9router.git" ;;
|
|
||||||
*)
|
|
||||||
echo "::error::Unsupported service '$SERVICE'"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "Waiting for $SERVICE ref $SHA in $REPO"
|
|
||||||
for attempt in {1..30}; do
|
|
||||||
if git ls-remote --exit-code "$REPO" "$SHA" >/dev/null 2>&1; then
|
|
||||||
echo "Submodule ref $SHA is fetchable for $SERVICE"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "Attempt $attempt/30: $SHA not visible yet; waiting 10s"
|
|
||||||
sleep 10
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "::error::Submodule ref $SHA for $SERVICE was not fetchable after 300s"
|
|
||||||
exit 1
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Make build wait for readiness job without blocking push/manual events**
|
|
||||||
|
|
||||||
Change build job header from:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
build:
|
|
||||||
needs: [changes]
|
|
||||||
```
|
|
||||||
|
|
||||||
to:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
build:
|
|
||||||
needs: [changes, wait-submodule-ref]
|
|
||||||
if: |
|
|
||||||
always() &&
|
|
||||||
needs.changes.result == 'success' &&
|
|
||||||
(needs.wait-submodule-ref.result == 'success' || needs.wait-submodule-ref.result == 'skipped') &&
|
|
||||||
needs.changes.outputs.matrix != '[]'
|
|
||||||
```
|
|
||||||
|
|
||||||
Remove existing build-level line:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
if: needs.changes.outputs.matrix != '[]'
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Make update-manifest wait for readiness job**
|
|
||||||
|
|
||||||
Change update-manifest header from:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
update-manifest:
|
|
||||||
needs: [changes, build]
|
|
||||||
if: |
|
|
||||||
always() &&
|
|
||||||
(needs.build.result == 'success' || needs.build.result == 'skipped')
|
|
||||||
```
|
|
||||||
|
|
||||||
to:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
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')
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Run YAML syntax check**
|
|
||||||
|
|
||||||
Run same command from Task 1 Step 3.
|
|
||||||
|
|
||||||
Expected both workflow files print `OK`.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .github/workflows/docker-build-push.yml
|
|
||||||
git commit -m "ci: wait for submodule refs before builds"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 3: Harden manifest update and submodule checkout
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `.github/workflows/docker-build-push.yml`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Add job permissions to build and manifest jobs**
|
|
||||||
|
|
||||||
Ensure build job contains:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
```
|
|
||||||
|
|
||||||
Ensure update-manifest job contains:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Replace dispatch submodule checkout block**
|
|
||||||
|
|
||||||
Inside `Update tags and submodules`, replace the repository_dispatch submodule update block with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Add pull/rebase retry before push**
|
|
||||||
|
|
||||||
Replace:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git commit -m "chore: update manifests and submodules [skip ci]"
|
|
||||||
git pull --rebase origin main
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Run YAML syntax check**
|
|
||||||
|
|
||||||
Run same command from Task 1 Step 3.
|
|
||||||
|
|
||||||
Expected both workflow files print `OK`.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .github/workflows/docker-build-push.yml
|
|
||||||
git commit -m "ci: harden manifest updates"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 4: Make deploy checkout submodule-free and least privilege
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `.github/workflows/deploy-docker.yml`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Add workflow-level read permissions**
|
|
||||||
|
|
||||||
After `concurrency`, add:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected shape:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
concurrency:
|
|
||||||
group: deploy-vps
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
```
|
|
||||||
|
|
||||||
If current `cancel-in-progress` is `true`, change it to `false`.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Make checkout non-recursive**
|
|
||||||
|
|
||||||
Replace checkout step:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
```
|
|
||||||
|
|
||||||
with:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
submodules: false
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Add deploy context log**
|
|
||||||
|
|
||||||
At start of `Deploy with Docker Compose on VPS` run script, after `set -euo pipefail`, add:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "Deploy event: ${{ github.event_name }}"
|
|
||||||
echo "Deploy ref: ${{ github.ref }}"
|
|
||||||
echo "Deploy sha: ${{ github.sha }}"
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: Run YAML syntax check**
|
|
||||||
|
|
||||||
Run same command from Task 1 Step 3.
|
|
||||||
|
|
||||||
Expected both workflow files print `OK`.
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .github/workflows/deploy-docker.yml
|
|
||||||
git commit -m "ci: avoid submodule checkout during deploy"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 5: Add GitHub Actions Dependabot updates
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `.github/dependabot.yml`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Add github-actions ecosystem**
|
|
||||||
|
|
||||||
Append this update entry under `updates:`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- package-ecosystem: 'github-actions'
|
|
||||||
directory: '/'
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
groups:
|
|
||||||
github-actions:
|
|
||||||
patterns:
|
|
||||||
- '*'
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected file shape:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: 'devcontainers'
|
|
||||||
directory: '/'
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
|
|
||||||
- package-ecosystem: 'github-actions'
|
|
||||||
directory: '/'
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
groups:
|
|
||||||
github-actions:
|
|
||||||
patterns:
|
|
||||||
- '*'
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Run YAML syntax check**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python - <<'PY'
|
|
||||||
from pathlib import Path
|
|
||||||
import yaml
|
|
||||||
paths = [Path('.github/dependabot.yml'), *Path('.github/workflows').glob('*.yml')]
|
|
||||||
for path in paths:
|
|
||||||
yaml.safe_load(path.read_text())
|
|
||||||
print(f'OK {path}')
|
|
||||||
PY
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected:
|
|
||||||
|
|
||||||
```text
|
|
||||||
OK .github/dependabot.yml
|
|
||||||
OK .github/workflows/deploy-docker.yml
|
|
||||||
OK .github/workflows/docker-build-push.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .github/dependabot.yml
|
|
||||||
git commit -m "ci: enable github actions dependency updates"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 6: Final validation
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Validate: `.github/workflows/docker-build-push.yml`
|
|
||||||
- Validate: `.github/workflows/deploy-docker.yml`
|
|
||||||
- Validate: `.github/dependabot.yml`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Run YAML syntax check**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python - <<'PY'
|
|
||||||
from pathlib import Path
|
|
||||||
import yaml
|
|
||||||
paths = [Path('.github/dependabot.yml'), *Path('.github/workflows').glob('*.yml')]
|
|
||||||
for path in paths:
|
|
||||||
yaml.safe_load(path.read_text())
|
|
||||||
print(f'OK {path}')
|
|
||||||
PY
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected all files print `OK`.
|
|
||||||
|
|
||||||
- [ ] **Step 2: Check workflows recognized by GitHub CLI**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gh workflow list
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected output includes:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Build and Push Docker Images
|
|
||||||
Deploy Docker to VPS
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: Inspect final diff**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git diff -- .github/workflows .github/dependabot.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected:
|
|
||||||
|
|
||||||
- `docker-build-push.yml` has dispatch validation, `wait-submodule-ref`, job permissions, and manifest push retry.
|
|
||||||
- `deploy-docker.yml` has non-recursive checkout and read-only permissions.
|
|
||||||
- `dependabot.yml` has `github-actions` updates.
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit any final validation fixes**
|
|
||||||
|
|
||||||
If Step 1 or Step 2 required fixes, commit them:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .github/workflows .github/dependabot.yml
|
|
||||||
git commit -m "ci: finalize production workflow hardening"
|
|
||||||
```
|
|
||||||
|
|
||||||
If no fixes were needed, do not create an empty commit.
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# React Direct Runtime and Images Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Serve the React SPA without nginx and remove frontend image-cache proxy calls so browser uses direct image/API URLs.
|
|
||||||
|
|
||||||
**Architecture:** The React Docker image still builds with Bun/Vite, but runtime uses `vite preview` from Bun instead of nginx. `CachedImage` normalizes image URLs and renders them directly, keeping fallback/retry UI but removing `/proxy/image-cache` auditing. Traefik keeps routing `react-web:80`, so compose and dynamic routing stay stable.
|
|
||||||
|
|
||||||
**Tech Stack:** Bun, Vite, React, TypeScript, Docker, Docker Compose, Traefik.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tasks
|
|
||||||
|
|
||||||
### Task 1: Switch React Runtime From nginx to Bun/Vite Preview
|
|
||||||
|
|
||||||
Modify `infra/docker/react.Dockerfile` so runtime stage uses `oven/bun:1-alpine`, installs production deps, copies `/app/dist`, exposes 80, and runs `bunx vite preview --host 0.0.0.0 --port 80`. Verify nginx runtime/copy lines are gone and Vite preview command exists.
|
|
||||||
|
|
||||||
### Task 2: Remove Image Cache Proxy From CachedImage
|
|
||||||
|
|
||||||
Modify `apps/react/src/components/ui/cached-image.tsx` to remove `API_BASE_URL` import, `/proxy/image-cache` POST, audit state/function, and auditing overlay. Keep direct normalized image URLs, retry behavior, and fallback image.
|
|
||||||
|
|
||||||
### Task 3: Verify Build and Config
|
|
||||||
|
|
||||||
Run static checks for both tasks, `bun --cwd apps/react run build`, `docker build -f infra/docker/react.Dockerfile -t react-web:test .`, start test container on `18080:80`, curl root, clean container, and run `git diff --check`.
|
|
||||||
|
|
||||||
### Task 4: Commit and Push
|
|
||||||
|
|
||||||
Commit changed files: `infra/docker/react.Dockerfile`, `apps/react/src/components/ui/cached-image.tsx`, `docs/superpowers/plans/2026-05-27-react-direct-runtime-and-images.md`. Push branch only after verification if user asks; do not push from worktree unless explicitly instructed.
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
# Moonrepo Migration Design
|
|
||||||
|
|
||||||
**Date:** 2026-05-24
|
|
||||||
**Status:** approved
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
Ultimate Asepharyana Tech is a polyglot monorepo with 7 applications managed as git submodules: nextjs (React/Next.js), elysia (Bun/Elysia), solidjs (SolidStart), rust (Axum), rust-auth (Axum auth service), leptos (Leptos WASM), and 9router. Nix flakes handle system dependencies and Docker image builds. Docker Compose manages deployment.
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
|
|
||||||
- No centralized task orchestration — running build/lint/test across all apps is manual and error-prone
|
|
||||||
- No dependency caching — builds are not incremental across apps
|
|
||||||
- Inconsistent developer experience — each app has its own conventions, onboarding is slow
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Adopt moonrepo for task orchestration, caching, and dependency graph management across all 7 apps while preserving the existing Nix build system, Docker Compose deployment, and git submodule structure.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
ultimate-asepharyana.tech/
|
|
||||||
├── .moon/
|
|
||||||
│ ├── workspace.yml
|
|
||||||
│ ├── toolchain.yml
|
|
||||||
│ └── tasks/
|
|
||||||
│ ├── typescript-build.yml
|
|
||||||
│ ├── typescript-lint.yml
|
|
||||||
│ ├── typescript-test.yml
|
|
||||||
│ ├── rust-build.yml
|
|
||||||
│ ├── rust-test.yml
|
|
||||||
│ └── rust-lint.yml
|
|
||||||
├── apps/
|
|
||||||
│ ├── nextjs/moon.yml
|
|
||||||
│ ├── elysia/moon.yml
|
|
||||||
│ ├── solidjs/moon.yml
|
|
||||||
│ ├── rust/moon.yml
|
|
||||||
│ ├── rust-auth/moon.yml
|
|
||||||
│ ├── leptos/moon.yml
|
|
||||||
│ └── 9router/moon.yml
|
|
||||||
├── .moon/workspace.yml # unchanged — Nix build + deployment
|
|
||||||
├── flake.nix # unchanged
|
|
||||||
├── infra/ # unchanged — Docker Compose, Traefik, nginx
|
|
||||||
└── scripts/ # unchanged
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tag Taxonomy
|
|
||||||
|
|
||||||
| Tag | Projects |
|
|
||||||
|-----|----------|
|
|
||||||
| `lang:typescript` | nextjs, elysia, solidjs |
|
|
||||||
| `lang:rust` | rust, rust-auth, leptos |
|
|
||||||
| `type:frontend` | nextjs, solidjs, leptos |
|
|
||||||
| `type:backend` | rust, elysia, rust-auth |
|
|
||||||
| `type:router` | 9router |
|
|
||||||
|
|
||||||
### Toolchain
|
|
||||||
|
|
||||||
- moonrepo manages Node 22, Bun, and TypeScript versions via `.moon/toolchain.yml` for consistent access across all TypeScript apps
|
|
||||||
- Rust toolchain remains managed by Nix/devShell — moonrepo does not touch Rust installation
|
|
||||||
- Nix devShell gains the moon CLI binary
|
|
||||||
|
|
||||||
### Project Dependencies
|
|
||||||
|
|
||||||
- **nextjs** depends on `rust-auth` and `elysia` (frontend consumes both APIs)
|
|
||||||
- **solidjs** depends on `elysia` and `rust-auth`
|
|
||||||
- **leptos** depends on `rust` (CSR frontend consumes rust backend API)
|
|
||||||
- **elysia**, **rust**, **rust-auth**, **9router** — no project dependencies
|
|
||||||
|
|
||||||
## Task Definitions
|
|
||||||
|
|
||||||
### Shared Tasks: TypeScript
|
|
||||||
|
|
||||||
All TypeScript apps inherit from `.moon/tasks/typescript-*.yml`:
|
|
||||||
|
|
||||||
| Task | Command | Inputs | Outputs |
|
|
||||||
|------|---------|--------|---------|
|
|
||||||
| `build` | `bun run build` | `src/**/*`, `tsconfig.json`, `package.json` | `.next`, `dist`, `.output` |
|
|
||||||
| `lint` | `bun run lint` | `src/**/*`, `eslint.config.mjs`, `tsconfig.json` | — |
|
|
||||||
| `typecheck` | `bun run check-types` | `src/**/*`, `tsconfig.json` | — |
|
|
||||||
| `test` | `bun test` | `src/**/*`, `test/**/*` | — |
|
|
||||||
| `e2e` | overridden per app | — | — |
|
|
||||||
|
|
||||||
### Shared Tasks: Rust
|
|
||||||
|
|
||||||
All Rust apps inherit from `.moon/tasks/rust-*.yml`, using system tasks:
|
|
||||||
|
|
||||||
| Task | Command | Inputs | Outputs |
|
|
||||||
|------|---------|--------|---------|
|
|
||||||
| `build` | `cargo build --release` | `src/**/*`, `Cargo.toml`, `Cargo.lock`, `build.rs` | `target/release/*` |
|
|
||||||
| `test` | `cargo test` | `src/**/*`, `Cargo.toml`, `Cargo.lock`, `tests/**/*` | — |
|
|
||||||
| `lint` | `cargo clippy -- -D warnings` | `src/**/*`, `Cargo.toml` | — |
|
|
||||||
| `fmt-check` | `cargo fmt --check` | `src/**/*` | — |
|
|
||||||
|
|
||||||
### Implicit Dependencies
|
|
||||||
|
|
||||||
Configured in `workspace.yml`: `lint` implicitly depends on `build` for TypeScript apps (typecheck path), and `build` propagates to dependents when source inputs change.
|
|
||||||
|
|
||||||
## Key Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
moon run :build # build all changed apps + dependents
|
|
||||||
moon run :lint # lint all
|
|
||||||
moon run :test # test all
|
|
||||||
moon run --tag lang:rust :test # test Rust apps only
|
|
||||||
moon run --affected :build # build only what changed
|
|
||||||
moon query projects --tag lang:typescript # list TypeScript projects
|
|
||||||
```
|
|
||||||
|
|
||||||
## What Does NOT Change
|
|
||||||
|
|
||||||
- **Nix flakes** — system dependencies, Docker image builds, devShell remain as-is
|
|
||||||
- **Docker Compose** — deployment continues through existing compose files in `infra/compose/`
|
|
||||||
- **Git submodules** — all 7 app repos stay as submodules under `apps/`
|
|
||||||
- **infra/** — Traefik and Docker Compose deployment remain under `infra/`; legacy Grafana/Prometheus/Alertmanager configs have been removed
|
|
||||||
- **scripts/** — existing utility scripts unchanged
|
|
||||||
|
|
||||||
## Migration Steps (High-Level)
|
|
||||||
|
|
||||||
1. Install moonrepo CLI, create `.moon/` scaffolding
|
|
||||||
2. Create shared task definitions in `.moon/tasks/`
|
|
||||||
3. Configure `workspace.yml` with project list, tags, and dependency constraints
|
|
||||||
4. Configure `toolchain.yml` for Node 22 + Bun
|
|
||||||
5. Add `moon.yml` to each of the 7 app directories
|
|
||||||
6. Validate: `moon check`, `moon run :build`, `moon run :lint`, `moon run :test`
|
|
||||||
7. Add moon CLI to Nix devShell
|
|
||||||
8. Commit and document
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
# PostgreSQL Migration Design
|
|
||||||
**Date:** 2026-05-25
|
|
||||||
**Scope:** Migrate elysia (Drizzle ORM + MySQL) and rust (SeaORM + MySQL) apps to PostgreSQL
|
|
||||||
**Approach:** Direct cutover (Approach B)
|
|
||||||
**Connection String:** `postgresql://asephs:hunterz@100.108.1.124:5432/hub`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Current State
|
|
||||||
|
|
||||||
### Elysia App
|
|
||||||
- **ORM:** Drizzle ORM v0.45.2
|
|
||||||
- **Driver:** mysql2 v3.20.0
|
|
||||||
- **Schema Location:** `apps/elysia/src/db/lib/schema.ts`
|
|
||||||
- **Tables:** users, accounts, sessions, roles, permissions, userRoles, and related junction tables
|
|
||||||
- **Database File:** `apps/elysia/src/db/lib/database.ts`
|
|
||||||
|
|
||||||
### Rust App
|
|
||||||
- **ORM:** SeaORM v1.1.19
|
|
||||||
- **Feature:** sqlx-mysql
|
|
||||||
- **DB Setup:** `apps/rust/src/infra/db_setup.rs`
|
|
||||||
- **Tables:** ImageCache (primary table managed by app)
|
|
||||||
- **Config:** Environment-based connection string
|
|
||||||
|
|
||||||
### Apps NOT Migrating
|
|
||||||
- 9router (uses SQLite runtime, excluded per requirements)
|
|
||||||
- nextjs (frontend, minimal DB usage)
|
|
||||||
- leptos, solidjs, rust-auth (frontends, no DB)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Migration Steps
|
|
||||||
|
|
||||||
### Phase 1: Pre-Migration (Preparation)
|
|
||||||
1. **Backup MySQL**
|
|
||||||
```bash
|
|
||||||
mysqldump -u <user> -p <db> > mysql_backup.sql
|
|
||||||
```
|
|
||||||
2. **Convert MySQL dump to PostgreSQL**
|
|
||||||
- Use `pgloader` or manual conversion for schema compatibility
|
|
||||||
- Handle type conversions:
|
|
||||||
- `INT` → `INTEGER`
|
|
||||||
- `VARCHAR(n)` → `VARCHAR(n)` (PostgreSQL compatible)
|
|
||||||
- `DATETIME` → `TIMESTAMP`
|
|
||||||
- `AUTO_INCREMENT` → `SERIAL` or `BIGSERIAL`
|
|
||||||
- Verify indexes and foreign keys convert correctly
|
|
||||||
3. **Test import into target PostgreSQL**
|
|
||||||
```bash
|
|
||||||
psql postgresql://asephs:hunterz@100.108.1.124:5432/hub < converted.sql
|
|
||||||
```
|
|
||||||
4. **Validate data integrity**
|
|
||||||
- Row counts match MySQL
|
|
||||||
- Indexes exist
|
|
||||||
- Foreign key constraints enforced
|
|
||||||
|
|
||||||
### Phase 2: Code Updates
|
|
||||||
|
|
||||||
#### Elysia App
|
|
||||||
1. **Update `apps/elysia/src/db/lib/database.ts`**
|
|
||||||
- Replace `mysql2` import with `postgres` driver
|
|
||||||
- Change Drizzle dialect from `drizzle-orm/mysql2` to `drizzle-orm/postgres`
|
|
||||||
- Update connection string format
|
|
||||||
|
|
||||||
2. **Update `apps/elysia/src/db/lib/schema.ts`**
|
|
||||||
- Replace `drizzle-orm/mysql-core` imports with `drizzle-orm/postgres-core`
|
|
||||||
- Change `mysqlTable` to `pgTable`
|
|
||||||
- Adjust column types if needed (e.g., `int` → `integer`)
|
|
||||||
|
|
||||||
3. **Update `apps/elysia/package.json`**
|
|
||||||
- Replace `mysql2` with `pg` (PostgreSQL driver)
|
|
||||||
- Keep `drizzle-orm` and `drizzle-kit` versions
|
|
||||||
|
|
||||||
4. **Update environment/config**
|
|
||||||
- Change `DATABASE_URL` to PostgreSQL connection string
|
|
||||||
|
|
||||||
#### Rust App
|
|
||||||
1. **Update `apps/rust/Cargo.toml`**
|
|
||||||
- Replace `sqlx-mysql` feature with `sqlx-postgres` in sea-orm dependency
|
|
||||||
- Add `postgres` feature if needed
|
|
||||||
|
|
||||||
2. **Update `apps/rust/src/infra/db_setup.rs`**
|
|
||||||
- Change `DbBackend::MySql` check to `DbBackend::Postgres`
|
|
||||||
- Adjust SQL syntax for PostgreSQL (e.g., index creation)
|
|
||||||
- Update error code handling (PostgreSQL uses different error codes)
|
|
||||||
|
|
||||||
3. **Update config/environment**
|
|
||||||
- Change `DATABASE_URL` to PostgreSQL connection string
|
|
||||||
|
|
||||||
### Phase 3: Cutover (Execution)
|
|
||||||
1. **Stop all apps**
|
|
||||||
```bash
|
|
||||||
# Stop elysia
|
|
||||||
# Stop rust app
|
|
||||||
```
|
|
||||||
2. **Export MySQL data**
|
|
||||||
```bash
|
|
||||||
mysqldump -u <user> -p <db> > final_backup.sql
|
|
||||||
```
|
|
||||||
3. **Convert and import to PostgreSQL**
|
|
||||||
```bash
|
|
||||||
# Convert dump
|
|
||||||
# Import to PostgreSQL
|
|
||||||
psql postgresql://asephs:hunterz@100.108.1.124:5432/hub < converted.sql
|
|
||||||
```
|
|
||||||
4. **Deploy updated apps**
|
|
||||||
- Deploy elysia with PostgreSQL driver
|
|
||||||
- Deploy rust with PostgreSQL feature
|
|
||||||
5. **Verify connectivity**
|
|
||||||
- Test database queries from both apps
|
|
||||||
- Check auth flow (users table)
|
|
||||||
- Verify session management
|
|
||||||
|
|
||||||
### Phase 4: Validation
|
|
||||||
1. **Smoke tests**
|
|
||||||
- User login/logout
|
|
||||||
- Session creation and retrieval
|
|
||||||
- Role/permission queries
|
|
||||||
- ImageCache operations (rust app)
|
|
||||||
2. **Data integrity checks**
|
|
||||||
- Row counts match pre-migration
|
|
||||||
- No orphaned foreign keys
|
|
||||||
- Indexes performing as expected
|
|
||||||
3. **Performance baseline**
|
|
||||||
- Compare query times MySQL vs PostgreSQL
|
|
||||||
- Monitor connection pool usage
|
|
||||||
|
|
||||||
### Phase 5: Rollback Plan (if needed)
|
|
||||||
1. **Stop apps**
|
|
||||||
2. **Restore MySQL from backup**
|
|
||||||
```bash
|
|
||||||
mysql -u <user> -p <db> < mysql_backup.sql
|
|
||||||
```
|
|
||||||
3. **Revert connection strings in apps**
|
|
||||||
4. **Redeploy with MySQL drivers**
|
|
||||||
5. **Restart apps**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Technical Details
|
|
||||||
|
|
||||||
### Elysia Driver Change
|
|
||||||
**Before:**
|
|
||||||
```typescript
|
|
||||||
import { drizzle } from 'drizzle-orm/mysql2'
|
|
||||||
import { createPool } from 'mysql2/promise'
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```typescript
|
|
||||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
||||||
import postgres from 'postgres'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rust Feature Change
|
|
||||||
**Before:**
|
|
||||||
```toml
|
|
||||||
sea-orm = { version = "1.1.19", features = ["sqlx-mysql", ...] }
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```toml
|
|
||||||
sea-orm = { version = "1.1.19", features = ["sqlx-postgres", ...] }
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Risk Assessment
|
|
||||||
|
|
||||||
| Risk | Mitigation |
|
|
||||||
|------|-----------|
|
|
||||||
| Data loss during migration | Full MySQL backup before cutover; test import first |
|
|
||||||
| App downtime | Cutover during low-traffic window; rollback plan ready |
|
|
||||||
| Connection string misconfiguration | Test connection before deploying apps |
|
|
||||||
| Schema incompatibilities | Pre-test conversion; validate indexes/constraints |
|
|
||||||
| Performance regression | Baseline MySQL performance; monitor PostgreSQL after cutover |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
- ✅ All data migrated to PostgreSQL (row counts match)
|
|
||||||
- ✅ Elysia app connects and queries work
|
|
||||||
- ✅ Rust app connects and queries work
|
|
||||||
- ✅ Auth flow functional (login/logout)
|
|
||||||
- ✅ No orphaned foreign keys
|
|
||||||
- ✅ Indexes present and performant
|
|
||||||
- ✅ Rollback plan tested and documented
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Timeline
|
|
||||||
|
|
||||||
- **Pre-migration:** 30 min (backup, convert, test)
|
|
||||||
- **Code updates:** 1-2 hours (driver changes, testing)
|
|
||||||
- **Cutover:** 15-30 min (stop apps, migrate data, restart)
|
|
||||||
- **Validation:** 30 min (smoke tests, data checks)
|
|
||||||
- **Total:** ~3-4 hours (including buffer)
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Hapus Nix, Docker-Only
|
|
||||||
|
|
||||||
**Goal:** Remove all Nix configuration and adapt CI/dev to Docker-only.
|
|
||||||
|
|
||||||
**Scope:**
|
|
||||||
- Delete: `flake.nix`, `flake.lock`, `nix/`, `.envrc`, `.direnv/`
|
|
||||||
- Modify: `.github/workflows/docker-build-push.yml` — remove Nix steps, move `rust-api` to Dockerfile build
|
|
||||||
- Modify: `.vscode/settings.json` — remove `nixEnvSelector`
|
|
||||||
- Modify: `README.md` — replace `nix build` with `docker build`
|
|
||||||
|
|
||||||
**CI Changes:**
|
|
||||||
- `rust-api` uses `infra/docker/rust.Dockerfile` (already exists)
|
|
||||||
- Remove `cachix/install-nix-action`, `cachix/cachix-action`, `nix build` step
|
|
||||||
- Remove `flake.nix`, `flake.lock`, `nix/**` from path triggers
|
|
||||||
- Update path filters: `nix/apps/*.nix` → `infra/docker/*.Dockerfile`
|
|
||||||
- Remove `repository_dispatch` Nix `--override-input` logic
|
|
||||||
- Add `rust-api` to Dockerfile case + push branch
|
|
||||||
|
|
||||||
**Dev Workflow:**
|
|
||||||
- Rust toolchain via `rustup`/`rust-toolchain.toml` in `apps/rust/`
|
|
||||||
- Dev via `docker compose` instead of `process-compose`
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
# Production GitHub Actions redesign
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Rework `.github/workflows` into a production baseline that balances reliability, security, speed, and clear failures while keeping automatic deploys from `main`.
|
|
||||||
|
|
||||||
## Current problems
|
|
||||||
|
|
||||||
- Deploy runner checks out submodules recursively even though deploy work happens on the VPS. Fresh submodule SHAs can fail with `not our ref` before deploy starts.
|
|
||||||
- Repository dispatch can update a parent submodule pointer before the submodule SHA is fetchable by GitHub Actions.
|
|
||||||
- Build and deploy permissions are broader than needed at workflow level.
|
|
||||||
- Failure logs do not clearly separate build, manifest update, submodule readiness, and deployment phases.
|
|
||||||
|
|
||||||
## Chosen approach
|
|
||||||
|
|
||||||
Use a split pipeline:
|
|
||||||
|
|
||||||
1. `docker-build-push.yml` remains the build and manifest update pipeline.
|
|
||||||
2. `deploy-docker.yml` remains deploy-only.
|
|
||||||
3. Repository dispatch waits for the requested submodule SHA to be fetchable before building and updating the parent pointer.
|
|
||||||
4. Deploy checkout no longer uses recursive submodules; the VPS updates submodules after resetting to `origin/main`.
|
|
||||||
|
|
||||||
## Build workflow design
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
- `push` to `main` for app/package/docker/workflow changes.
|
|
||||||
- `repository_dispatch` with `service` and `sha` payload.
|
|
||||||
- `workflow_dispatch` for manual full builds.
|
|
||||||
|
|
||||||
Jobs:
|
|
||||||
|
|
||||||
- `changes`: detects the service matrix and validates dispatch payloads.
|
|
||||||
- `build`: builds and pushes only selected service images using Docker Buildx registry cache.
|
|
||||||
- `update-manifest`: updates compose image tags and, for dispatch events, updates the matching submodule pointer.
|
|
||||||
|
|
||||||
Repository dispatch handling:
|
|
||||||
|
|
||||||
- For dispatch events, wait until `git ls-remote` or equivalent fetch confirms the payload SHA exists in the service submodule remote.
|
|
||||||
- Retry for a bounded timeout and fail with an explicit message if the SHA never becomes visible.
|
|
||||||
- Only after readiness is confirmed, checkout the SHA in the submodule and commit the parent pointer update.
|
|
||||||
|
|
||||||
## Deploy workflow design
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
- `workflow_run` from successful `Build and Push Docker Images` on `main`.
|
|
||||||
- `push` to `main` for `infra/compose/**` and deploy workflow changes.
|
|
||||||
- `workflow_dispatch`.
|
|
||||||
|
|
||||||
Behavior:
|
|
||||||
|
|
||||||
- Keep automatic deploy from `main`.
|
|
||||||
- Keep a single deploy concurrency group.
|
|
||||||
- Use non-recursive checkout on the runner.
|
|
||||||
- On the VPS, fetch/reset `origin/main`, update submodules, compute changed compose stacks, pull images, and run Docker Compose.
|
|
||||||
|
|
||||||
## Permissions and action trust
|
|
||||||
|
|
||||||
Defaults:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
```
|
|
||||||
|
|
||||||
Job-specific permissions:
|
|
||||||
|
|
||||||
- Build job: `contents: read`, `packages: write`.
|
|
||||||
- Manifest update job: `contents: write`.
|
|
||||||
- Deploy job: `contents: read`.
|
|
||||||
|
|
||||||
Action pinning:
|
|
||||||
|
|
||||||
- GitHub-owned and Docker official actions may use major versions such as `actions/checkout@v4` and `docker/build-push-action@v6`.
|
|
||||||
- Any future third-party action should be pinned to a full commit SHA.
|
|
||||||
|
|
||||||
## Reliability details
|
|
||||||
|
|
||||||
- Keep `set -euo pipefail` in shell steps.
|
|
||||||
- Add bounded retry around submodule SHA readiness.
|
|
||||||
- Add clear logs for selected services, image tags, compose files changed, and dispatch payload values.
|
|
||||||
- Avoid recursive submodule checkout in deploy to eliminate fresh-SHA checkout race.
|
|
||||||
- Manifest commits continue using `[skip ci]` to prevent build loops.
|
|
||||||
|
|
||||||
## Speed details
|
|
||||||
|
|
||||||
- Keep selective matrix builds.
|
|
||||||
- Keep Docker Buildx registry cache.
|
|
||||||
- Keep deploy checkout shallow and submodule-free.
|
|
||||||
- Avoid rebuilding from compose-only manifest commits.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
Before marking implementation complete:
|
|
||||||
|
|
||||||
- Validate workflow YAML syntax.
|
|
||||||
- Run `gh workflow list` or equivalent sanity checks.
|
|
||||||
- Verify build workflow still detects React updates.
|
|
||||||
- Verify deploy workflow no longer fails during runner checkout for fresh submodule SHAs.
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
# Monorepo Restructure: ultimate-asepharyana.tech → asepharyana-hub
|
|
||||||
|
|
||||||
**Date:** 2026-07-09
|
|
||||||
**Status:** Approved
|
|
||||||
**Owner:** @asepharyana
|
|
||||||
|
|
||||||
## Background
|
|
||||||
|
|
||||||
Proyek ini sebelumnya bernama `ultimate-asepharyana.tech` — sebuah monorepo yang berisi beberapa service aplikasi, infrastruktur, dokumentasi, dan utility scripts. Karena GitHub org `MythEclipse` kena banned, semua remote repos tidak bisa diakses. Perlu dilakukan restruktur dan rename project secara menyeluruh.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
|
|
||||||
1. Rename project dari `ultimate-asepharyana.tech` ke `asepharyana-hub`
|
|
||||||
2. Hapus service docker-manager dan teleuploader dari proyek
|
|
||||||
3. Hapus semua pointer `.git` submodule (clean slate)
|
|
||||||
4. Update `.gitmodules` dengan remote baru ke `github.com/asepharyana/*`
|
|
||||||
5. Update semua referensi: workflows, konfigurasi, dokumentasi, image names
|
|
||||||
6. Siapkan struktur lokal — inisialisasi git dan push dilakukan terpisah
|
|
||||||
|
|
||||||
## Service yang tetap dipertahankan
|
|
||||||
|
|
||||||
| Service | Path | Git remote baru |
|
|
||||||
|---------|------|----------------|
|
|
||||||
| Elysia API | `apps/elysia` | `asepharyana/asepharyana-hub-elysia` |
|
|
||||||
| React Frontend | `apps/react` | `asepharyana/asepharyana-hub-react` |
|
|
||||||
| Scraper | `apps/scraper` | `asepharyana/asepharyana-hub-scraper` |
|
|
||||||
| Rust Auth | `apps/rust-auth` | `asepharyana/asepharyana-hub-rust-auth` |
|
|
||||||
|
|
||||||
## Service yang dihapus
|
|
||||||
|
|
||||||
| Service | Path |
|
|
||||||
|---------|------|
|
|
||||||
| Docker Manager | `apps/docker-manager/` |
|
|
||||||
| TeleUploader | `apps/teleuploader/` |
|
|
||||||
|
|
||||||
## File yang akan dihapus
|
|
||||||
|
|
||||||
- `apps/docker-manager/` (seluruh direktori)
|
|
||||||
- `apps/teleuploader/` (seluruh direktori)
|
|
||||||
- `infra/compose/docker-manager.yml`
|
|
||||||
- `infra/compose/teleuploader.yml`
|
|
||||||
- `infra/docker/docker-manager.Dockerfile`
|
|
||||||
- `infra/docker/teleuploader.Dockerfile`
|
|
||||||
|
|
||||||
## File pointer `.git` submodule yang dihapus
|
|
||||||
|
|
||||||
- `apps/elysia/.git`
|
|
||||||
- `apps/react/.git`
|
|
||||||
- `apps/scraper/.git`
|
|
||||||
- `apps/rust-auth/.git`
|
|
||||||
|
|
||||||
## Rename mapping
|
|
||||||
|
|
||||||
| Lokasi | Dari | Ke |
|
|
||||||
|--------|------|----|
|
|
||||||
| `package.json` `name` | `ultimate-asepharyana.tech` | `asepharyana-hub` |
|
|
||||||
| `README.md` | judul & path references | `asepharyana-hub` |
|
|
||||||
| `ARCHITECTURE.md` | directory tree, paths | `asepharyana-hub` |
|
|
||||||
| `CONTRIBUTING.md` | repo names & paths | `asepharyana-hub-*` |
|
|
||||||
| `infra/README.md` | deskripsi | `asepharyana-hub` |
|
|
||||||
| `.env.example` | `TRAEFIK_CONFIG_PATH` | `asepharyana-hub` |
|
|
||||||
| Perlengkapan infra Traefik | path config references | `asepharyana-hub` |
|
|
||||||
| `scripts/cleanup-ghcr.sh` | `MythEclipse` | `asepharyana` |
|
|
||||||
| `docs/add-new-app.md` | path references | `asepharyana-hub` |
|
|
||||||
| `docs/adr/*.md` | path references | `asepharyana-hub` |
|
|
||||||
|
|
||||||
## Image name migration (GHCR)
|
|
||||||
|
|
||||||
| Lama | Baru |
|
|
||||||
|------|------|
|
|
||||||
| `ghcr.io/mytheclipse/ultimate-asepharyana.tech/elysia-api:*` | `ghcr.io/asepharyana/asepharyana-hub/elysia-api:*` |
|
|
||||||
| `ghcr.io/mytheclipse/ultimate-asepharyana.tech/react-web:*` | `ghcr.io/asepharyana/asepharyana-hub/react-web:*` |
|
|
||||||
| `ghcr.io/mytheclipse/ultimate-asepharyana.tech/scraper-api:*` | `ghcr.io/asepharyana/asepharyana-hub/scraper-api:*` |
|
|
||||||
| `ghcr.io/mytheclipse/ultimate-asepharyana.tech/rust-auth:*` | `ghcr.io/asepharyana/asepharyana-hub/rust-auth:*` |
|
|
||||||
| `ghcr.io/mytheclipse/ultimate-asepharyana.tech/docker-manager:*` | — (dihapus) |
|
|
||||||
| `ghcr.io/mytheclipse/ultimate-asepharyana.tech/teleuploader:*` | — (dihapus) |
|
|
||||||
|
|
||||||
## Workflow changes
|
|
||||||
|
|
||||||
### `docker-build-push.yml`
|
|
||||||
- `IMAGE_NAME_PREFIX`: `mytheclipse/ultimate-asepharyana.tech` → `asepharyana/asepharyana-hub`
|
|
||||||
- Hapus service entries: `docker-manager`, `teleuploader`
|
|
||||||
- Update repo URLs di `wait-submodule-ref` dari `MythEclipse/*` ke `asepharyana/*`
|
|
||||||
- Update `update-manifest` phase — hapus service docker-manager & teleuploader
|
|
||||||
|
|
||||||
### `deploy-docker.yml`
|
|
||||||
- Update `git remote add origin`
|
|
||||||
- Hapus docker-manager & teleuploader dari compose list dan checkout
|
|
||||||
|
|
||||||
### `update-submodule.yml`
|
|
||||||
- Hapus service docker-manager & teleuploader
|
|
||||||
- Update nama workflow
|
|
||||||
|
|
||||||
## `.gitmodules`
|
|
||||||
|
|
||||||
Hanya berisi 4 apps dengan remote baru:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[submodule "apps/elysia"]
|
|
||||||
path = apps/elysia
|
|
||||||
url = https://github.com/asepharyana/asepharyana-hub-elysia.git
|
|
||||||
[submodule "apps/react"]
|
|
||||||
path = apps/react
|
|
||||||
url = https://github.com/asepharyana/asepharyana-hub-react.git
|
|
||||||
[submodule "apps/scraper"]
|
|
||||||
path = apps/scraper
|
|
||||||
url = https://github.com/asepharyana/asepharyana-hub-scraper.git
|
|
||||||
[submodule "apps/rust-auth"]
|
|
||||||
path = apps/rust-auth
|
|
||||||
url = https://github.com/asepharyana/asepharyana-hub-rust-auth.git
|
|
||||||
```
|
|
||||||
|
|
||||||
## Execution plan
|
|
||||||
|
|
||||||
1. Hapus docker-manager & teleuploader direktori + file infra
|
|
||||||
2. Hapus `.git` pointer di submodule
|
|
||||||
3. Update `.gitmodules`
|
|
||||||
4. Update `package.json`
|
|
||||||
5. Update `README.md`, `ARCHITECTURE.md`, `CONTRIBUTING.md`
|
|
||||||
6. Update `infra/compose/*.yml` image tags
|
|
||||||
7. Update `.env.example`, `infra/README.md`, docs traefik
|
|
||||||
8. Update `docker-build-push.yml`
|
|
||||||
9. Update `deploy-docker.yml`
|
|
||||||
10. Update `update-submodule.yml`
|
|
||||||
11. Update `scripts/cleanup-ghcr.sh`
|
|
||||||
12. Update `docs/add-new-app.md`
|
|
||||||
13. Update `docs/adr/*.md` path references (historical)
|
|
||||||
|
|
||||||
## Post-execution state
|
|
||||||
|
|
||||||
- Root direktori `asepharyana-hub/` dengan source code apps utuh (tanpa git)
|
|
||||||
- 4 app submodule terdaftar di `.gitmodules` dengan remote baru
|
|
||||||
- Infra/docs/scripts tetap menyatu di root
|
|
||||||
- 0 references ke `MythEclipse/ultimate-asepharyana.tech` di file konfigurasi
|
|
||||||
- Siap untuk `git init && git add && git commit` kapan saja
|
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
# Tailscale Networking
|
||||||
|
|
||||||
|
Dokumentasi setup dan troubleshooting konektivitas Tailscale antara node `orangevps` (VPS) dan `imrnes` (bare-metal).
|
||||||
|
|
||||||
|
## Topologi
|
||||||
|
|
||||||
|
```
|
||||||
|
orangevps (VPS)
|
||||||
|
├─ Tailscale IP: 100.x.x.x (dynamic)
|
||||||
|
├─ Public IP: 45.127.35.244
|
||||||
|
├─ Docker containers (app-shared-net)
|
||||||
|
│ └─ perlu akses ke imrnes via Tailscale
|
||||||
|
└─ tailscale-routes.service
|
||||||
|
└─ menambahkan route 100.x.x.x ke tabel routing main
|
||||||
|
|
||||||
|
imrnes (Bare-metal)
|
||||||
|
├─ Tailscale IP: 100.121.180.82
|
||||||
|
├─ Layanan:
|
||||||
|
│ ├─ PostgreSQL (port 6432)
|
||||||
|
│ └─ Redis (port 6379)
|
||||||
|
└─ Layanan hanya listen di Tailscale interface
|
||||||
|
```
|
||||||
|
|
||||||
|
## Masalah: Container Tidak Bisa Mencapai Tailscale IP
|
||||||
|
|
||||||
|
Docker container secara default hanya bisa mencapai IP di Docker bridge network dan network host. Tailscale menggunakan interface virtual `tailscale0` yang tidak secara otomatis di-route ke container.
|
||||||
|
|
||||||
|
### Solusi: `tailscale-routes.service`
|
||||||
|
|
||||||
|
Service systemd yang menambahkan route Tailscale ke tabel routing `main` agar traffic dari container bisa melewati host ke Tailscale.
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# /etc/systemd/system/tailscale-routes.service
|
||||||
|
[Unit]
|
||||||
|
Description=Add Tailscale routes to main routing table
|
||||||
|
After=tailscaled.service
|
||||||
|
Requires=tailscaled.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
RemainAfterExit=yes
|
||||||
|
ExecStart=/bin/sh -c 'ip rule add from all lookup main priority 10000 2>/dev/null; ip route add 100.64.0.0/10 dev tailscale0 table main 2>/dev/null || true'
|
||||||
|
ExecStop=/bin/sh -c 'ip rule del from all lookup main priority 10000 2>/dev/null; ip route del 100.64.0.0/10 dev tailscale0 table main 2>/dev/null || true'
|
||||||
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verifikasi
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek apakah route sudah ada
|
||||||
|
ip route show table main | grep tailscale
|
||||||
|
|
||||||
|
# Test dari dalam container
|
||||||
|
docker run --rm alpine ping -c 3 100.121.180.82
|
||||||
|
|
||||||
|
# Test koneksi PostgreSQL dari container
|
||||||
|
docker run --rm alpine sh -c "apk add postgresql-client && psql -h 100.121.180.82 -p 6432 -U asephs -d hub -c 'SELECT 1'"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup Tailscale di Node Baru
|
||||||
|
|
||||||
|
### 1. Install Tailscale
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://tailscale.com/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Authenticate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo tailscale up --advertise-routes=<LAN_SUBNET_CIDR>
|
||||||
|
```
|
||||||
|
|
||||||
|
Untuk node yang hanya sebagai client (tidak advertise routes):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo tailscale up
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Enable dan Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable --now tailscaled
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Setup Route Service (khusus node dengan Docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Buat service file
|
||||||
|
sudo nano /etc/systemd/system/tailscale-routes.service
|
||||||
|
# Paste content di atas
|
||||||
|
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now tailscale-routes.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Konfigurasi ACL di Tailscale Admin
|
||||||
|
|
||||||
|
Pastikan ACL di [Tailscale Admin Console](https://login.tailscale.com/admin/acls) mengizinkan traffic antar node:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"acls": [
|
||||||
|
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Atau jika ingin lebih ketat:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"acls": [
|
||||||
|
{"action": "accept", "src": ["tag:server"], "dst": ["tag:server:*"]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konfigurasi iptables/ufw
|
||||||
|
|
||||||
|
Pastikan port yang diperlukan terbuka di `imrnes`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PostgreSQL
|
||||||
|
sudo ufw allow in on tailscale0 to any port 6432 proto tcp
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
sudo ufw allow in on tailscale0 to any port 6379 proto tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
Atau menggunakan iptables langsung:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo iptables -A INPUT -i tailscale0 -p tcp --dport 6432 -j ACCEPT
|
||||||
|
sudo iptables -A INPUT -i tailscale0 -p tcp --dport 6379 -j ACCEPT
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Container timeout connect ke Tailscale IP
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Cek apakah route service berjalan
|
||||||
|
systemctl status tailscale-routes.service
|
||||||
|
|
||||||
|
# 2. Cek route di host
|
||||||
|
ip route show table main | grep 100.64
|
||||||
|
|
||||||
|
# 3. Cek apakah host bisa ping ke target
|
||||||
|
ping 100.121.180.82
|
||||||
|
|
||||||
|
# 4. Test dari container dengan --network host
|
||||||
|
docker run --rm --network host alpine ping -c 3 100.121.180.82
|
||||||
|
|
||||||
|
# 5. Pastikan tidak ada firewall blocking
|
||||||
|
iptables -L FORWARD -n -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tailscale disconnect
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek status
|
||||||
|
tailscale status
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
sudo systemctl restart tailscaled
|
||||||
|
```
|
||||||
|
|
||||||
|
### IP Tailscale berubah
|
||||||
|
|
||||||
|
Tailscale IP bisa berubah jika node dire-auth. Update:
|
||||||
|
|
||||||
|
1. `.env` production di VPS (via GitHub secret `ENV_FILE_PRODUCTION`)
|
||||||
|
2. Database connection strings
|
||||||
|
3. Redis connection strings
|
||||||
|
4. Trigger redeploy
|
||||||
|
|
||||||
|
### MagicDNS tidak resolve
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek DNS
|
||||||
|
tailscale dns status
|
||||||
|
|
||||||
|
# Flush DNS cache
|
||||||
|
sudo resolvectl flush-caches
|
||||||
|
```
|
||||||
|
|
||||||
|
## Catatan Keamanan
|
||||||
|
|
||||||
|
- Interface Tailscale (`tailscale0`) hanya boleh diakses oleh node yang terautentikasi dalam network yang sama
|
||||||
|
- Jangan expose port database ke public interface (`eth0`), hanya ke Tailscale
|
||||||
|
- Gunakan ACL untuk membatasi akses antar node jika diperlukan
|
||||||
|
- Rotate auth key secara berkala di Tailscale admin console
|
||||||
@@ -0,0 +1,494 @@
|
|||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
Kumpulan solusi untuk masalah umum di infrastruktur `asepharyana/infra` (orangevps).
|
||||||
|
|
||||||
|
> **Catatan (2026-08-02):** Produksi sekarang Caddy + Nix/systemd. Section Traefik/Docker di bawah adalah LEGACY — Docker dan Traefik dihapus dari produksi; gunakan hanya sebagai referensi historis.
|
||||||
|
|
||||||
|
## Daftar Isi
|
||||||
|
|
||||||
|
- [Deployment](#deployment)
|
||||||
|
- [Dapr](#dapr)
|
||||||
|
- [NATS](#nats)
|
||||||
|
- [Tailscale / Networking](#tailscale--networking)
|
||||||
|
- [Caddy](#caddy)
|
||||||
|
- [Database](#database)
|
||||||
|
- [Submodule](#submodule)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Workflow deploy gagal: "Secrets not fully configured"
|
||||||
|
|
||||||
|
**Penyebab:** Salah satu GitHub secrets tidak diset.
|
||||||
|
|
||||||
|
**Solusi:** Cek secrets di Settings > Secrets and variables > Actions:
|
||||||
|
|
||||||
|
| Secret | Status |
|
||||||
|
|--------|--------|
|
||||||
|
| `SSH_PRIVATE_KEY` | Wajib |
|
||||||
|
| `VPS_HOST` | Wajib (`45.127.35.244`) |
|
||||||
|
| `VPS_USER` | Wajib (`root`) |
|
||||||
|
| ~~`VPS_TARGET_DIR`~~ | Tidak dipakai lagi (CI baru tidak checkout VPS) |
|
||||||
|
| `ENV_FILE_PRODUCTION` | Wajib |
|
||||||
|
|
||||||
|
### Workflow build gagal: "Submodule commit not fetchable"
|
||||||
|
|
||||||
|
**Penyebab:** Commit SHA dari `repository_dispatch` belum tersedia di remote submodule repo (eventual consistency).
|
||||||
|
|
||||||
|
**Solusi:** Workflow akan retry hingga 5 menit. Jika masih gagal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek apakah commit ada di remote
|
||||||
|
git ls-remote https://github.com/asepharyana/scraper.git <SHA>
|
||||||
|
|
||||||
|
# Trigger ulang dispatch dari submodule repo, atau push langsung ke hub
|
||||||
|
```
|
||||||
|
|
||||||
|
### Push manifest gagal: conflict di main
|
||||||
|
|
||||||
|
**Penyebab:** Ada commit lain yang masuk sebelum workflow selesai.
|
||||||
|
|
||||||
|
**Solusi:** Workflow otomatis retry rebase 3 kali. Jika semua gagal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Manual fix di lokal
|
||||||
|
git pull --rebase origin main
|
||||||
|
# resolve conflict
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dapr
|
||||||
|
|
||||||
|
### Dapr sidecar tidak connect ke placement
|
||||||
|
|
||||||
|
**Gejala:** Container `scraper-api-dapr` restart loop. Log: `failed to connect to placement`
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek log sidecar
|
||||||
|
docker logs scraper-api-dapr --tail 50
|
||||||
|
|
||||||
|
# Cek apakah placement service running
|
||||||
|
docker ps -a | grep dapr-placement
|
||||||
|
docker logs dapr-placement --tail 20
|
||||||
|
|
||||||
|
# Cek konektivitas
|
||||||
|
docker exec scraper-api-dapr curl -s http://dapr-placement:50005
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restart placement dulu, lalu sidecar
|
||||||
|
docker compose -f infra/compose/dapr.yml up -d --force-recreate
|
||||||
|
sleep 5
|
||||||
|
docker compose -f infra/compose/scraper.yml up -d --force-recreate scraper-api-dapr
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dapr pub/sub tidak bekerja
|
||||||
|
|
||||||
|
**Gejala:** Event di-publish tapi tidak sampai ke subscriber.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek komponen Dapr
|
||||||
|
curl http://localhost:3500/v1.0/components
|
||||||
|
|
||||||
|
# Cek health sidecar
|
||||||
|
curl http://localhost:3500/v1.0/healthz
|
||||||
|
|
||||||
|
# Cek Redis (backend pub/sub)
|
||||||
|
docker exec redis redis-cli ping
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restart sidecar
|
||||||
|
docker restart scraper-api-dapr
|
||||||
|
|
||||||
|
# Jika Redis bermasalah, restart juga
|
||||||
|
docker restart redis
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dapr state store error: "key not found"
|
||||||
|
|
||||||
|
**Penyebab:** Key belum ada di state store, atau prefix berbeda.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek state langsung di Redis
|
||||||
|
docker exec redis redis-cli KEYS 'dapr*'
|
||||||
|
|
||||||
|
# State store menggunakan prefix "dapr"
|
||||||
|
# Format key: dapr || <app-id> || <key>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NATS
|
||||||
|
|
||||||
|
### NATS tidak bisa start
|
||||||
|
|
||||||
|
**Gejala:** Container NATS restart loop.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs nats --tail 50
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:** Kemungkinan korupsi data JetStream:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup dulu volume data
|
||||||
|
docker run --rm -v nats_data:/data -v /tmp:/backup alpine cp -r /data /backup/nats_data_backup
|
||||||
|
|
||||||
|
# Hapus volume dan recreate
|
||||||
|
docker compose -f infra/compose/nats.yml down
|
||||||
|
docker volume rm asepharyana-hub_nats_data
|
||||||
|
docker compose -f infra/compose/nats.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### JetStream stream overflow
|
||||||
|
|
||||||
|
**Gejala:** Disk penuh, NATS lambat.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek ukuran volume
|
||||||
|
docker system df | grep nats_data
|
||||||
|
du -sh /var/lib/docker/volumes/nats_data/_data/
|
||||||
|
|
||||||
|
# Cek stream info
|
||||||
|
nats stream list
|
||||||
|
nats stream info <stream-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Purge stream tertentu (data hilang)
|
||||||
|
nats stream purge <stream-name>
|
||||||
|
|
||||||
|
# Atau tambah limit stream via NATS config
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Slow Consumer" warning
|
||||||
|
|
||||||
|
**Gejala:** Log NATS menampilkan "slow consumer".
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8222/varz | jq '.slow_consumers'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
- Scale consumer (tambah worker)
|
||||||
|
- Percepat processing message
|
||||||
|
- Kurangi ukuran payload
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Traefik
|
||||||
|
|
||||||
|
### Traefik tidak routing ke service
|
||||||
|
|
||||||
|
**Gejala:** 404 atau 503 dari Traefik.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek apakah service container running
|
||||||
|
docker ps -a | grep scraper-api
|
||||||
|
|
||||||
|
# Cek log Traefik
|
||||||
|
docker logs traefik --tail 50
|
||||||
|
|
||||||
|
# Cek apakah container ada di network yang benar
|
||||||
|
docker network inspect app-shared-net | grep scraper-api
|
||||||
|
|
||||||
|
# Test routing langsung
|
||||||
|
curl -H "Host: scraper.asepharyana.my.id" http://localhost/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pastikan service terdaftar di apps.yaml
|
||||||
|
# Pastikan container join app-shared-net
|
||||||
|
# Restart Traefik
|
||||||
|
docker compose -f infra/compose/traefik.yml up -d --force-recreate
|
||||||
|
```
|
||||||
|
|
||||||
|
### TLS certificate error
|
||||||
|
|
||||||
|
**Gejala:** Browser menampilkan warning certificate.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek sertifikat di host
|
||||||
|
ls -la /root/asepharyana.my.id.pem
|
||||||
|
openssl x509 -in /root/asepharyana.my.id.pem -text -noout | head -20
|
||||||
|
|
||||||
|
# Cek apakah Traefik bisa mount
|
||||||
|
docker exec traefik ls -la /etc/traefik/certs/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
- Update sertifikat di host
|
||||||
|
- Restart Traefik
|
||||||
|
- Jika path berbeda, set environment variable `TRAEFIK_CERT_*`
|
||||||
|
|
||||||
|
### Rate limit terlalu ketat
|
||||||
|
|
||||||
|
**Gejala:** Request legitimate di-block.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek rate-limit config di middlewares.yaml
|
||||||
|
# Current: average 100, burst 50
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:** Ubah nilai `average` dan `burst` di `infra/traefik/dynamic/middlewares.yaml`, lalu reload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker kill --signal HUP traefik
|
||||||
|
# atau
|
||||||
|
docker exec traefik kill -HUP 1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tailscale / Networking
|
||||||
|
|
||||||
|
### Container tidak bisa connect ke Tailscale IP
|
||||||
|
|
||||||
|
**Gejala:** Timeout saat container connect ke `100.121.180.82:6432`.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek route service dari host
|
||||||
|
systemctl status tailscale-routes.service
|
||||||
|
|
||||||
|
# Cek route di host
|
||||||
|
ip route show table main | grep 100.64
|
||||||
|
|
||||||
|
# Cek koneksi dari host
|
||||||
|
ping 100.121.180.82
|
||||||
|
|
||||||
|
# Test dari container (dengan --network host)
|
||||||
|
docker run --rm --network host alpine ping -c 3 100.121.180.82
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restart route service
|
||||||
|
sudo systemctl restart tailscale-routes.service
|
||||||
|
|
||||||
|
# Atau tambah route manual
|
||||||
|
sudo ip rule add from all lookup main priority 10000
|
||||||
|
sudo ip route add 100.64.0.0/10 dev tailscale0 table main
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database connection refused
|
||||||
|
|
||||||
|
**Gejala:** Service tidak bisa konek ke PostgreSQL.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek apakah DB listen di Tailscale (dari imrnes)
|
||||||
|
ss -tlnp | grep 6432
|
||||||
|
|
||||||
|
# Cek dari orangevps
|
||||||
|
nc -zv 100.121.180.82 6432
|
||||||
|
|
||||||
|
# Cek firewall di imrnes
|
||||||
|
sudo ufw status
|
||||||
|
sudo iptables -L -n | grep 6432
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Di imrnes: pastikan PostgreSQL bind ke Tailscale interface
|
||||||
|
# Di postgresql.conf:
|
||||||
|
listen_addresses = 'localhost,100.121.180.82'
|
||||||
|
|
||||||
|
# Di pg_hba.conf:
|
||||||
|
host hub asephs 100.0.0.0/8 md5
|
||||||
|
|
||||||
|
# Restart PostgreSQL
|
||||||
|
sudo systemctl restart postgresql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis connection refused dari container
|
||||||
|
|
||||||
|
**Gejala:** Service tidak bisa connect ke `redis://redis:6379`.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek apakah container Redis running
|
||||||
|
docker ps -a | grep redis
|
||||||
|
|
||||||
|
# Cek apakah container target join network yang sama
|
||||||
|
docker inspect <container> | grep -A5 Networks
|
||||||
|
|
||||||
|
# Cek DNS resolve dari container
|
||||||
|
docker exec <container> getent hosts redis
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pastikan Redis ada di network app-shared-net
|
||||||
|
docker network inspect app-shared-net | grep redis
|
||||||
|
|
||||||
|
# Jika tidak, attach
|
||||||
|
docker network connect app-shared-net redis
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker / Container
|
||||||
|
|
||||||
|
### Container restart loop
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs <container> --tail 50
|
||||||
|
docker inspect <container> | jq '.[].State'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Penyebab umum:**
|
||||||
|
- Health check gagal
|
||||||
|
- Dependency service belum siap
|
||||||
|
- Environment variable tidak diset
|
||||||
|
|
||||||
|
### Image pull gagal dari GHCR
|
||||||
|
|
||||||
|
**Gejala:** `docker pull` gagal di VPS.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek login
|
||||||
|
cat ~/.docker/config.json | grep ghcr
|
||||||
|
|
||||||
|
# Cek visibility package
|
||||||
|
# Buka https://github.com/orgs/asepharyana/packages
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Re-login
|
||||||
|
echo $GITHUB_TOKEN | docker login ghcr.io -u asepharyana --password-stdin
|
||||||
|
|
||||||
|
# Pastikan package visibility public atau di-share ke org
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disk penuh
|
||||||
|
|
||||||
|
**Gejala:** Container crash, write error.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
df -h
|
||||||
|
docker system df
|
||||||
|
du -sh /var/lib/docker/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Bersihkan container/image/volume yang tidak dipakai
|
||||||
|
docker system prune -a -f
|
||||||
|
|
||||||
|
# Hapus image lama
|
||||||
|
docker image prune -a -f
|
||||||
|
|
||||||
|
# Lihat volume terbesar
|
||||||
|
docker system df -v | grep -E "(nats_data|redis_data)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
### Koneksi PostgreSQL lambat
|
||||||
|
|
||||||
|
**Gejala:** Query time high, connection timeout.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dari container, test latency
|
||||||
|
docker exec scraper-api ping -c 5 100.121.180.82
|
||||||
|
|
||||||
|
# Cek koneksi aktif
|
||||||
|
docker exec scraper-api psql $DATABASE_URL -c "SELECT count(*) FROM pg_stat_activity;"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
- Cek Tailscale latency
|
||||||
|
- Adjust connection pool size
|
||||||
|
- Cek resource PostgreSQL di `imrnes`
|
||||||
|
|
||||||
|
### Migration gagal
|
||||||
|
|
||||||
|
**Gejala:** Service error setelah image update.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek log service
|
||||||
|
docker logs scraper-api --tail 100 | grep -i migration
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
- Migration ada di submodule `apps/scraper`, bukan di hub
|
||||||
|
- Pastikan schema sesuai dengan versi code
|
||||||
|
- Rollback image jika migration tidak backward-compatible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Submodule
|
||||||
|
|
||||||
|
### HEAD detached di submodule
|
||||||
|
|
||||||
|
**Gejala:** `git status` di `apps/scraper` menunjukkan "HEAD detached".
|
||||||
|
|
||||||
|
**Penyebab:** Normal. Submodule selalu dalam keadaan detached HEAD karena mengacu pada commit spesifik.
|
||||||
|
|
||||||
|
**Solusi:** Jangan commit perubahan dari dalam submodule. Selalu bekerja di repo asli.
|
||||||
|
|
||||||
|
### Submodule tidak ter-update setelah pull
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
|
||||||
|
### Konflik submodule saat rebase/merge
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Resolve dengan memilih versi yang benar
|
||||||
|
git add apps/scraper
|
||||||
|
git rebase --continue
|
||||||
|
```
|
||||||
+28
-86
@@ -1,98 +1,40 @@
|
|||||||
# Infrastructure
|
# Infra — Reverse Proxy & VPS Config
|
||||||
|
|
||||||
Docker Compose and Traefik configuration for `asepharyana-hub`.
|
Config produksi untuk **orangevps** (Caddy reverse proxy + firewall + systemd drop-ins).
|
||||||
|
|
||||||
## Layout
|
## Caddy (`caddy/`)
|
||||||
|
|
||||||
```text
|
- `Caddyfile.prod` — **source of truth** untuk `/etc/caddy/Caddyfile`.
|
||||||
infra/
|
- Site pattern: `<service>.asepharyana.my.id` / `.web.id` → `import proxy <port>`.
|
||||||
├── compose/ # One compose file per stack/service
|
- Auto-TLS Let's Encrypt; HTTP/3 default.
|
||||||
│ ├── traefik.yml # Public reverse proxy
|
- Deploy: push `infra/caddy/**` ke `main` → workflow `caddy-deploy.yml` sync + reload + verify.
|
||||||
│ ├── shared.yml # Shared Redis
|
|
||||||
│ ├── react.yml # React SPA
|
### Update site baru
|
||||||
│ ├── scraper.yml # Scraper API
|
```caddyfile
|
||||||
│ ├── elysia.yml # Elysia API
|
myservice.asepharyana.my.id {
|
||||||
│ └── rust-auth.yml # Rust auth API
|
import proxy 4022
|
||||||
├── docker/ # Dockerfiles and image runtime helpers
|
}
|
||||||
├── traefik/ # Static and dynamic Traefik configuration
|
|
||||||
│ ├── dynamic/ # Routers, services, middlewares, TLS certs
|
|
||||||
│ └── TRAEFIK_ENV_CONFIG.md
|
|
||||||
└── config/ # Service bootstrap configuration
|
|
||||||
```
|
```
|
||||||
|
Commit + push → CI reload caddy → cek `curl -sI https://myservice.asepharyana.my.id`.
|
||||||
|
|
||||||
Archived configs that are not deployed live under `docs/config/`.
|
## Firewall (`firewall/`)
|
||||||
|
|
||||||
## First-time setup
|
- `firewall.sh` — deny-by-default iptables:
|
||||||
|
- Public: 22 (SSH), 80/443 (Caddy), 4013 (hermes dashboard), 25565 (Minecraft via TCPShield only)
|
||||||
|
- Tailscale CGNAT `100.64.0.0/10`: semua port
|
||||||
|
- Localhost: semua; sisanya DROP + logged.
|
||||||
|
- `99-hardening.conf`, `99-ssh-optimization.conf` — sysctl drop-ins.
|
||||||
|
|
||||||
Create the shared Docker network before starting any service:
|
## Prometheus (`prometheus/`)
|
||||||
|
|
||||||
```bash
|
- `targets.yml` — file_sd targets untuk Prometheus (node-exporter, app /metrics).
|
||||||
docker network create app-shared-net
|
|
||||||
```
|
|
||||||
|
|
||||||
Create `.env` from `.env.example` and fill production values. Do not commit `.env`.
|
## systemd (`systemd/`)
|
||||||
|
|
||||||
## Deployment order
|
- `scraper-otel.conf` — drop-in OTEL exporter untuk `scraper.service`.
|
||||||
|
|
||||||
The GitHub deploy workflow combines the active compose files automatically. For manual deployment, use this order:
|
## Deployment Model
|
||||||
|
|
||||||
```bash
|
Repo `asepharyana/infra` TIDAK build aplikasi. Aplikasi deploy via repo masing-masing
|
||||||
docker compose -f infra/compose/shared.yml up -d
|
(`hub`, `scraper`, `tools`, `llm-api`) dengan `nix build → nix copy → nix-env --profile → systemctl restart`.
|
||||||
docker compose -f infra/compose/traefik.yml up -d
|
Repo ini hanya mengelola config yang di-sync manual/CI ke VPS.
|
||||||
|
|
||||||
docker compose \
|
|
||||||
-f infra/compose/react.yml \
|
|
||||||
-f infra/compose/scraper.yml \
|
|
||||||
-f infra/compose/elysia.yml \
|
|
||||||
-f infra/compose/rust-auth.yml \
|
|
||||||
up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment variables
|
|
||||||
|
|
||||||
Common variables used by infra compose files:
|
|
||||||
|
|
||||||
```env
|
|
||||||
DATABASE_URL=
|
|
||||||
GITHUB_TOKEN=
|
|
||||||
JWT_SECRET=
|
|
||||||
SHARED_REDIS_EXPOSE=127.0.0.1:6379:6379
|
|
||||||
```
|
|
||||||
|
|
||||||
Traefik certificate path variables are optional because `infra/compose/traefik.yml` provides production-compatible defaults. See `infra/traefik/TRAEFIK_ENV_CONFIG.md` for the full list.
|
|
||||||
|
|
||||||
## Traefik
|
|
||||||
|
|
||||||
Traefik reads dynamic config from `infra/traefik/dynamic/`:
|
|
||||||
|
|
||||||
- `apps.yaml` — routers and upstream services
|
|
||||||
- `middlewares.yaml` — shared middleware chains
|
|
||||||
- `ssl.yaml` — TLS certificates
|
|
||||||
|
|
||||||
The primary certificate intentionally pairs `asephstech.pem` with `asephscloud.key` to preserve the current production layout.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
Run syntax checks after editing infra YAML:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python - <<'PY'
|
|
||||||
import pathlib, yaml
|
|
||||||
for path in pathlib.Path('infra').rglob('*.yml'):
|
|
||||||
with path.open() as fh:
|
|
||||||
yaml.safe_load(fh)
|
|
||||||
print(f'OK {path}')
|
|
||||||
for path in pathlib.Path('infra').rglob('*.yaml'):
|
|
||||||
with path.open() as fh:
|
|
||||||
yaml.safe_load(fh)
|
|
||||||
print(f'OK {path}')
|
|
||||||
PY
|
|
||||||
```
|
|
||||||
|
|
||||||
Check compose rendering when Docker is available:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
for f in infra/compose/*.yml; do
|
|
||||||
docker compose -f "$f" config >/dev/null && echo "OK $f"
|
|
||||||
done
|
|
||||||
```
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user