chore: initial hub repo structure
This commit is contained in:
@@ -0,0 +1 @@
|
||||
1.3.11
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/universal:2",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/rust:1": {},
|
||||
"ghcr.io/devcontainers/features/node:2": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
**/.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,13 @@
|
||||
# Editor configuration, see http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1,11 @@
|
||||
# Default owners
|
||||
* @MythEclipse
|
||||
|
||||
# Infrastructure
|
||||
/infra/ @MythEclipse
|
||||
|
||||
# GitHub config
|
||||
/.github/ @MythEclipse
|
||||
|
||||
# Docs
|
||||
/docs/ @MythEclipse
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Create a report to help us improve
|
||||
title: '[Bug] '
|
||||
labels: bug
|
||||
assignees: asepharyana
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
## Steps to Reproduce
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. Scroll down to '...'
|
||||
4. See error
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
## Actual Behavior
|
||||
|
||||
A clear and concise description of what actually happened, including any error messages, logs, or screenshots.
|
||||
|
||||
## Environment
|
||||
|
||||
- OS: [e.g. Ubuntu 22.04, macOS 14.0]
|
||||
- Browser (if applicable): [e.g. Chrome 120, Firefox 121]
|
||||
- Version / Commit: [e.g. v1.0.0, commit 3a2b1c4]
|
||||
- Runtime: [e.g. Node 20, Docker 24]
|
||||
|
||||
## Additional Context
|
||||
|
||||
Add any other context about the problem here, such as relevant log output, stack traces, or screenshots.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea for this project
|
||||
title: '[Feature] '
|
||||
labels: enhancement
|
||||
assignees: asepharyana
|
||||
---
|
||||
|
||||
## Problem Description
|
||||
|
||||
A clear and concise description of the problem or need this feature would address. Ex. I'm always frustrated when [...]
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
A clear and concise description of what you would like to happen, including any specific functionality, API changes, or user interface considerations.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
A clear and concise description of any alternative solutions or features you have considered and why they were not sufficient.
|
||||
|
||||
## Additional Context
|
||||
|
||||
Add any other context, mockups, or examples about the feature request here.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Only the latest stable release of this project receives security updates. Older versions are not supported.
|
||||
|
||||
| Version | Supported |
|
||||
| -------- | ------------------ |
|
||||
| latest | :white_check_mark: |
|
||||
| < latest | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability, please report it via [GitHub Security Advisories](https://github.com/asepharyana/asepharyana-hub/security/advisories).
|
||||
|
||||
Do **not** report security vulnerabilities via public GitHub issues.
|
||||
|
||||
Please include the following information in your report:
|
||||
|
||||
- A description of the vulnerability and the potential impact
|
||||
- Steps to reproduce the issue
|
||||
- The version(s) affected
|
||||
- Any possible mitigations you have identified
|
||||
|
||||
## Disclosure Policy
|
||||
|
||||
- We will acknowledge receipt of your report within **48 hours**.
|
||||
- We will investigate the issue and provide an estimated timeline for a fix.
|
||||
- Once a fix is released, we will publish a security advisory and credit the reporter (if desired).
|
||||
- We ask that you allow us **90 days** from the date of the report to release a fix before any public disclosure.
|
||||
@@ -0,0 +1,21 @@
|
||||
# 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
|
||||
updates:
|
||||
- package-ecosystem: 'devcontainers'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: weekly
|
||||
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: weekly
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- '*'
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: Pull Request
|
||||
about: Submit a pull request to improve the project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
A clear and concise description of the changes being introduced and why they are needed. Include any relevant motivation or context.
|
||||
|
||||
## Type of Change
|
||||
|
||||
Please delete options that are not relevant.
|
||||
|
||||
- [ ] **feat** -- A new feature
|
||||
- [ ] **fix** -- A bug fix
|
||||
- [ ] **chore** -- Maintenance, dependency updates, build configuration
|
||||
- [ ] **docs** -- Documentation only changes
|
||||
- [ ] **refactor** -- A code change that neither fixes a bug nor adds a feature
|
||||
|
||||
## How Has This Been Tested?
|
||||
|
||||
Please describe the tests that you ran to verify your changes.
|
||||
|
||||
- [ ] Unit tests pass
|
||||
- [ ] Integration / E2E tests pass
|
||||
- [ ] Manual testing performed (describe below)
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] My code follows the project's coding style and conventions
|
||||
- [ ] I have performed a self-review of my own changes
|
||||
- [ ] I have added or updated tests to cover my changes (if applicable)
|
||||
- [ ] All new and existing tests pass locally
|
||||
- [ ] I have updated the documentation (if applicable)
|
||||
- [ ] My changes generate no new warnings, linter errors, or type errors
|
||||
- [ ] Any dependent changes have been merged and published
|
||||
|
||||
## Related Issues
|
||||
|
||||
Fixes # (issue)
|
||||
|
||||
Closes # (issue)
|
||||
|
||||
## Screenshots (if applicable)
|
||||
|
||||
Add screenshots to help explain your changes.
|
||||
@@ -0,0 +1,253 @@
|
||||
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
|
||||
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."
|
||||
@@ -0,0 +1,331 @@
|
||||
name: Build and Push Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'apps/**'
|
||||
- 'packages/**'
|
||||
- '.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
|
||||
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(/|$)|packages(/|$)|\.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
|
||||
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
|
||||
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
|
||||
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,29 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'apps/*/src/**/*.ts'
|
||||
- 'apps/*/src/**/*.tsx'
|
||||
- 'apps/*/eslint.config.mjs'
|
||||
- 'eslint.config.mjs'
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'apps/*/src/**/*.ts'
|
||||
- 'apps/*/src/**/*.tsx'
|
||||
- 'eslint.config.mjs'
|
||||
|
||||
jobs:
|
||||
eslint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- run: npm install -g eslint @antfu/eslint-config
|
||||
- run: eslint . --no-error-on-unmatched-pattern || echo "Lint check completed (best-effort)"
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Security
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '0 6 * * 1' # Every Monday
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
- uses: github/codeql-action/analyze@v3
|
||||
@@ -0,0 +1,22 @@
|
||||
name: TypeCheck
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'apps/**/*.ts'
|
||||
- 'apps/**/*.tsx'
|
||||
- 'tsconfig.base.json'
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- run: |
|
||||
cd apps/react && npm install && npx tsc --noEmit || echo "TypeScript check completed"
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Update Submodule Pointer
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [submodule-updated]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Update submodule pointer
|
||||
env:
|
||||
SERVICE: ${{ github.event.client_payload.service }}
|
||||
SHA: ${{ github.event.client_payload.sha }}
|
||||
run: |
|
||||
echo "Updating ${SERVICE} to ${SHA}"
|
||||
git submodule update --init "apps/${SERVICE}"
|
||||
cd "apps/${SERVICE}"
|
||||
# unshallow → full fetch so we get tree objects for the target SHA
|
||||
git fetch --depth=1000 origin master
|
||||
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}"
|
||||
git push
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
apps/gmw/
|
||||
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
dist
|
||||
tmp
|
||||
out-tsc
|
||||
error.log
|
||||
# dependencies
|
||||
node_modules
|
||||
.bun/**
|
||||
.turbo/
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
**/target/**
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.claude
|
||||
# Next.js
|
||||
.next
|
||||
out
|
||||
**/.codegraph/**
|
||||
**/.claude/**
|
||||
test-output
|
||||
**/**.env
|
||||
**/**.env.**
|
||||
vite.config.*.timestamp*
|
||||
vitest.config.*.timestamp*
|
||||
|
||||
storybook-static
|
||||
~/.bun/**
|
||||
docs/dependency-map.md
|
||||
docs/handoff-log.jsonl
|
||||
docs/observability.md
|
||||
docs/quality-gates.json
|
||||
docs/workflow-state.json
|
||||
docs/todo.md
|
||||
**/vendor/
|
||||
|
||||
.codegraph/
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
[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
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"next-devtools": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "next-devtools-mcp@latest"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
# 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.
|
||||
@@ -0,0 +1 @@
|
||||
22.11.0
|
||||
@@ -0,0 +1,6 @@
|
||||
# Add files here to ignore them from prettier formatting
|
||||
/dist
|
||||
/coverage
|
||||
*.env
|
||||
**/target/
|
||||
**/node_modules/
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"semi": true,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"nrwl.angular-console",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"ms-playwright.playwright",
|
||||
"firsttris.vscode-jest-runner"
|
||||
]
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{}
|
||||
+332
@@ -0,0 +1,332 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,3 @@
|
||||
# Authors
|
||||
|
||||
- Asep Haryana Saputra (@asepharyana) - Project maintainer
|
||||
@@ -0,0 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
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/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- Restructured the repository into a lightweight hub repo with standalone app submodules.
|
||||
- Simplified root tooling to plain `package.json` scripts and per-service commands.
|
||||
- Kept infrastructure, deployment workflows, and documentation in the root hub repo.
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed deprecated services from apps, compose files, Dockerfiles, Traefik routes, and workflows.
|
||||
- Removed stale monorepo orchestration configs and hook tooling from the root repo.
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
# Contributing to Asepharyana Hub
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Local Setup](#local-setup)
|
||||
- [Development Workflow](#development-workflow)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Coding Standards](#coding-standards)
|
||||
- [Commit Message Format](#commit-message-format)
|
||||
- [Pull Request Process](#pull-request-process)
|
||||
- [Adding a New Service](#adding-a-new-service)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Git** with LFS support
|
||||
- **Node.js** >= 22.11.0 (via `.node-version` or `.nvmrc`)
|
||||
- **Bun** >= 1.3.11 (package manager)
|
||||
- **Rust** >= 1.89.0 (for Rust services)
|
||||
- **Docker** and **Docker Compose** (for shared infrastructure)
|
||||
|
||||
## Local Setup
|
||||
|
||||
### 1. Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/asepharyana/asepharyana-hub.git
|
||||
cd asepharyana-hub
|
||||
```
|
||||
|
||||
### 2. Initialize Submodules
|
||||
|
||||
This hub repo uses Git submodules for all application services:
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
This checks out all submodules at the pinned commit (not `main`). The submodules and their remotes are:
|
||||
|
||||
| Path | Remote |
|
||||
| ---------------- | --------------------------------------- |
|
||||
| `apps/elysia` | `asepharyana/asepharyana-hub-elysia` |
|
||||
| `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
|
||||
|
||||
Install dependencies for TypeScript/Bun services:
|
||||
|
||||
```bash
|
||||
cd apps/elysia && bun install && cd ../..
|
||||
cd apps/react && npm install && cd ../..
|
||||
```
|
||||
|
||||
### 4. Start Shared Infrastructure
|
||||
|
||||
Start shared services (Redis) via Docker Compose:
|
||||
|
||||
```bash
|
||||
docker compose -f infra/compose/shared.yml up -d
|
||||
```
|
||||
|
||||
### 5. Configure Environment
|
||||
|
||||
Copy the example environment file and adjust as needed:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Key variables to configure:
|
||||
|
||||
| Variable | Description |
|
||||
| -------------- | ----------------------------------------------------- |
|
||||
| `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 |
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Running Services
|
||||
|
||||
**Rust API (rust-auth):**
|
||||
|
||||
```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
|
||||
|
||||
- Rust OpenAPI: `http://localhost:4091/docs`
|
||||
- Elysia Swagger: `http://localhost:4092/docs`
|
||||
- Elysia AsyncAPI: `http://localhost:4092/docs-ws`
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### Linting
|
||||
|
||||
- **ESLint** with `@antfu/eslint-config` for TypeScript/JavaScript
|
||||
- **Cargo Clippy** for Rust
|
||||
|
||||
Run linting:
|
||||
|
||||
```bash
|
||||
# TypeScript/JavaScript
|
||||
eslint . --no-error-on-unmatched-pattern
|
||||
|
||||
# Rust specific
|
||||
cd apps/rust-auth && cargo clippy -- -D warnings
|
||||
```
|
||||
|
||||
### Formatting
|
||||
|
||||
- **Prettier** for TypeScript/JavaScript/Markdown (config in `.prettierrc`)
|
||||
- Single quotes, 100 print width, 2-space indent, trailing commas
|
||||
- **Cargo fmt** for Rust
|
||||
- **EditorConfig** for general formatting (`.editorconfig`)
|
||||
|
||||
```bash
|
||||
# Prettier
|
||||
prettier --write .
|
||||
|
||||
# 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
|
||||
|
||||
This project enforces **Conventional Commits** for all commit messages.
|
||||
|
||||
### Format
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
| Type | Usage |
|
||||
| ---------- | ----------------------------------------------------------- |
|
||||
| `feat` | A new feature |
|
||||
| `fix` | A bug fix |
|
||||
| `chore` | Maintenance, config, tooling changes |
|
||||
| `docs` | Documentation only changes |
|
||||
| `refactor` | Code change that neither fixes a bug nor adds a feature |
|
||||
| `test` | Adding or updating tests |
|
||||
| `ci` | CI/CD configuration and scripts |
|
||||
| `style` | Formatting, missing semicolons, etc. (no production change) |
|
||||
| `perf` | Performance improvement |
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
feat(rust-auth): add OAuth2 Google login flow
|
||||
fix(elysia): handle null JWT payload in auth middleware
|
||||
chore: update eslint config to v10
|
||||
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
|
||||
```
|
||||
|
||||
### Scopes
|
||||
|
||||
Common scopes: `rust-auth`, `elysia`, `react`, `scraper`, `infra`, `ci`, `deps`
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. **Create a branch** from `main` with a descriptive name:
|
||||
- `feat/my-feature`
|
||||
- `fix/issue-description`
|
||||
- `chore/update-config`
|
||||
|
||||
2. **Make your changes** following the coding standards above.
|
||||
|
||||
3. **Run checks locally** before pushing:
|
||||
|
||||
```bash
|
||||
cd apps/react && npx tsc --noEmit
|
||||
eslint . --no-error-on-unmatched-pattern
|
||||
```
|
||||
|
||||
4. **Push and open a PR** against `main`. CI will automatically run:
|
||||
- **Lint** — ESLint across changed TypeScript files
|
||||
- **TypeCheck** — TypeScript compilation check
|
||||
- **Security** — CodeQL analysis (weekly schedule + PRs)
|
||||
|
||||
5. **Docker Build Pipeline** triggers on pushes to `main` when `apps/**` changes:
|
||||
- Detects which services changed
|
||||
- Builds Docker images for only those services
|
||||
- Pushes to GHCR with `latest` and `sha-<short>` tags
|
||||
- Updates compose manifests to use the new SHA tags
|
||||
|
||||
6. **Deployment Pipeline** triggers after a successful Docker build:
|
||||
- SSHes into the VPS (`orange`, Tailscale IP `100.96.248.86`)
|
||||
- Pulls updated Docker images
|
||||
- Recreates only the changed containers
|
||||
- All services share the `app-shared-net` Docker network
|
||||
|
||||
7. **Merge** after CI passes and you have at least one approval (if applicable). Use squash merge to keep history clean.
|
||||
|
||||
## Adding a New Service
|
||||
|
||||
See `docs/add-new-app.md` for the complete step-by-step guide. In summary:
|
||||
|
||||
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
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Asep Haryana Saputra
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,32 @@
|
||||
.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 ESLint across the repo
|
||||
eslint . --no-error-on-unmatched-pattern
|
||||
|
||||
format: ## Format code with Prettier
|
||||
prettier --write "**/*.{ts,tsx,js,jsx,json,md,yaml,yml}"
|
||||
|
||||
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
|
||||
@@ -0,0 +1,201 @@
|
||||
# Asepharyana Hub
|
||||
|
||||
Hub repo untuk ekosistem portfolio dan layanan pendukung milik Asep Haryana Saputra.
|
||||
Aplikasi dipisah sebagai submodule agar frontend, API, dan service pendukung bisa dikembangkan serta di-deploy secara independen.
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Path | Default Local Port | Notes |
|
||||
| :---------- | :--------------- | :----------------- | :---------------------------------------------------------------------------- |
|
||||
| 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
|
||||
|
||||
File compose berada di `infra/compose/`:
|
||||
|
||||
- `traefik.yml`: reverse proxy Traefik untuk semua layanan.
|
||||
- `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/`.
|
||||
|
||||
## Docker Image Builds
|
||||
|
||||
Build image via Dockerfile:
|
||||
|
||||
```bash
|
||||
docker build -f infra/docker/rust.Dockerfile -t rust-auth:latest .
|
||||
docker build -f infra/docker/elysia.Dockerfile -t elysia-api:latest .
|
||||
docker build -f infra/docker/react.Dockerfile -t react-web:latest .
|
||||
docker build -f infra/docker/scraper.Dockerfile -t scraper-api:latest .
|
||||
```
|
||||
|
||||
Tag and push:
|
||||
|
||||
```bash
|
||||
SHORT_SHA=$(git rev-parse --short HEAD)
|
||||
|
||||
docker tag rust-auth:latest ghcr.io/asepharyana/asepharyana-hub/rust-auth:sha-$SHORT_SHA
|
||||
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
|
||||
@@ -0,0 +1,75 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.env.local
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
/generated/prisma
|
||||
apps/gmw/
|
||||
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
dist
|
||||
tmp
|
||||
out-tsc
|
||||
error.log
|
||||
# dependencies
|
||||
node_modules
|
||||
.bun/**
|
||||
.turbo/
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
**/target/**
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.claude
|
||||
# Next.js
|
||||
.next
|
||||
out
|
||||
**/.codegraph/**
|
||||
**/.claude/**
|
||||
test-output
|
||||
**/**.env
|
||||
**/**.env.**
|
||||
vite.config.*.timestamp*
|
||||
vitest.config.*.timestamp*
|
||||
|
||||
|
||||
storybook-static
|
||||
~/.bun/**
|
||||
docs/dependency-map.md
|
||||
docs/handoff-log.jsonl
|
||||
docs/observability.md
|
||||
docs/quality-gates.json
|
||||
docs/workflow-state.json
|
||||
docs/todo.md
|
||||
**/vendor/
|
||||
|
||||
# moonrepo
|
||||
.moon/cache
|
||||
.~moon**
|
||||
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
dist
|
||||
*.tsbuildinfo
|
||||
.env*
|
||||
.claude/
|
||||
.codegraph/
|
||||
.code-review-graph/
|
||||
.superpowers/
|
||||
.remember/
|
||||
@@ -0,0 +1,7 @@
|
||||
target/
|
||||
.env
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
**/.codegraph/**
|
||||
**/.claude/**
|
||||
@@ -0,0 +1,67 @@
|
||||
apps/gmw/
|
||||
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
dist
|
||||
tmp
|
||||
out-tsc
|
||||
error.log
|
||||
# dependencies
|
||||
node_modules
|
||||
.bun/**
|
||||
.turbo/
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
**/target/**
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.claude
|
||||
# Next.js
|
||||
.next
|
||||
out
|
||||
**/.codegraph/**
|
||||
**/.claude/**
|
||||
test-output
|
||||
**/**.env
|
||||
**/**.env.**
|
||||
vite.config.*.timestamp*
|
||||
vitest.config.*.timestamp*
|
||||
|
||||
|
||||
storybook-static
|
||||
~/.bun/**
|
||||
docs/dependency-map.md
|
||||
docs/handoff-log.jsonl
|
||||
docs/observability.md
|
||||
docs/quality-gates.json
|
||||
docs/workflow-state.json
|
||||
docs/todo.md
|
||||
**/vendor/
|
||||
|
||||
# moonrepo
|
||||
.moon/cache
|
||||
.~moon**
|
||||
@@ -0,0 +1,488 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "ultimate-asepharyana.tech",
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.7.1",
|
||||
"@commitlint/config-conventional": "^19.7.1",
|
||||
"eslint": "^9.18.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.4.3",
|
||||
"prettier": "^3.4.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@commitlint/cli": ["@commitlint/cli@19.8.1", "", { "dependencies": { "@commitlint/format": "^19.8.1", "@commitlint/lint": "^19.8.1", "@commitlint/load": "^19.8.1", "@commitlint/read": "^19.8.1", "@commitlint/types": "^19.8.1", "tinyexec": "^1.0.0", "yargs": "^17.0.0" }, "bin": { "commitlint": "./cli.js" } }, "sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA=="],
|
||||
|
||||
"@commitlint/config-conventional": ["@commitlint/config-conventional@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "conventional-changelog-conventionalcommits": "^7.0.2" } }, "sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ=="],
|
||||
|
||||
"@commitlint/config-validator": ["@commitlint/config-validator@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "ajv": "^8.11.0" } }, "sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ=="],
|
||||
|
||||
"@commitlint/ensure": ["@commitlint/ensure@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "lodash.camelcase": "^4.3.0", "lodash.kebabcase": "^4.1.1", "lodash.snakecase": "^4.1.1", "lodash.startcase": "^4.4.0", "lodash.upperfirst": "^4.3.1" } }, "sha512-mXDnlJdvDzSObafjYrOSvZBwkD01cqB4gbnnFuVyNpGUM5ijwU/r/6uqUmBXAAOKRfyEjpkGVZxaDsCVnHAgyw=="],
|
||||
|
||||
"@commitlint/execute-rule": ["@commitlint/execute-rule@19.8.1", "", {}, "sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA=="],
|
||||
|
||||
"@commitlint/format": ["@commitlint/format@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "chalk": "^5.3.0" } }, "sha512-kSJj34Rp10ItP+Eh9oCItiuN/HwGQMXBnIRk69jdOwEW9llW9FlyqcWYbHPSGofmjsqeoxa38UaEA5tsbm2JWw=="],
|
||||
|
||||
"@commitlint/is-ignored": ["@commitlint/is-ignored@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "semver": "^7.6.0" } }, "sha512-AceOhEhekBUQ5dzrVhDDsbMaY5LqtN8s1mqSnT2Kz1ERvVZkNihrs3Sfk1Je/rxRNbXYFzKZSHaPsEJJDJV8dg=="],
|
||||
|
||||
"@commitlint/lint": ["@commitlint/lint@19.8.1", "", { "dependencies": { "@commitlint/is-ignored": "^19.8.1", "@commitlint/parse": "^19.8.1", "@commitlint/rules": "^19.8.1", "@commitlint/types": "^19.8.1" } }, "sha512-52PFbsl+1EvMuokZXLRlOsdcLHf10isTPlWwoY1FQIidTsTvjKXVXYb7AvtpWkDzRO2ZsqIgPK7bI98x8LRUEw=="],
|
||||
|
||||
"@commitlint/load": ["@commitlint/load@19.8.1", "", { "dependencies": { "@commitlint/config-validator": "^19.8.1", "@commitlint/execute-rule": "^19.8.1", "@commitlint/resolve-extends": "^19.8.1", "@commitlint/types": "^19.8.1", "chalk": "^5.3.0", "cosmiconfig": "^9.0.0", "cosmiconfig-typescript-loader": "^6.1.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "lodash.uniq": "^4.5.0" } }, "sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A=="],
|
||||
|
||||
"@commitlint/message": ["@commitlint/message@19.8.1", "", {}, "sha512-+PMLQvjRXiU+Ae0Wc+p99EoGEutzSXFVwQfa3jRNUZLNW5odZAyseb92OSBTKCu+9gGZiJASt76Cj3dLTtcTdg=="],
|
||||
|
||||
"@commitlint/parse": ["@commitlint/parse@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "conventional-changelog-angular": "^7.0.0", "conventional-commits-parser": "^5.0.0" } }, "sha512-mmAHYcMBmAgJDKWdkjIGq50X4yB0pSGpxyOODwYmoexxxiUCy5JJT99t1+PEMK7KtsCtzuWYIAXYAiKR+k+/Jw=="],
|
||||
|
||||
"@commitlint/read": ["@commitlint/read@19.8.1", "", { "dependencies": { "@commitlint/top-level": "^19.8.1", "@commitlint/types": "^19.8.1", "git-raw-commits": "^4.0.0", "minimist": "^1.2.8", "tinyexec": "^1.0.0" } }, "sha512-03Jbjb1MqluaVXKHKRuGhcKWtSgh3Jizqy2lJCRbRrnWpcM06MYm8th59Xcns8EqBYvo0Xqb+2DoZFlga97uXQ=="],
|
||||
|
||||
"@commitlint/resolve-extends": ["@commitlint/resolve-extends@19.8.1", "", { "dependencies": { "@commitlint/config-validator": "^19.8.1", "@commitlint/types": "^19.8.1", "global-directory": "^4.0.1", "import-meta-resolve": "^4.0.0", "lodash.mergewith": "^4.6.2", "resolve-from": "^5.0.0" } }, "sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg=="],
|
||||
|
||||
"@commitlint/rules": ["@commitlint/rules@19.8.1", "", { "dependencies": { "@commitlint/ensure": "^19.8.1", "@commitlint/message": "^19.8.1", "@commitlint/to-lines": "^19.8.1", "@commitlint/types": "^19.8.1" } }, "sha512-Hnlhd9DyvGiGwjfjfToMi1dsnw1EXKGJNLTcsuGORHz6SS9swRgkBsou33MQ2n51/boIDrbsg4tIBbRpEWK2kw=="],
|
||||
|
||||
"@commitlint/to-lines": ["@commitlint/to-lines@19.8.1", "", {}, "sha512-98Mm5inzbWTKuZQr2aW4SReY6WUukdWXuZhrqf1QdKPZBCCsXuG87c+iP0bwtD6DBnmVVQjgp4whoHRVixyPBg=="],
|
||||
|
||||
"@commitlint/top-level": ["@commitlint/top-level@19.8.1", "", { "dependencies": { "find-up": "^7.0.0" } }, "sha512-Ph8IN1IOHPSDhURCSXBz44+CIu+60duFwRsg6HqaISFHQHbmBtxVw4ZrFNIYUzEP7WwrNPxa2/5qJ//NK1FGcw=="],
|
||||
|
||||
"@commitlint/types": ["@commitlint/types@19.8.1", "", { "dependencies": { "@types/conventional-commits-parser": "^5.0.0", "chalk": "^5.3.0" } }, "sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
|
||||
|
||||
"@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
|
||||
|
||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||
|
||||
"@types/conventional-commits-parser": ["@types/conventional-commits-parser@5.0.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="],
|
||||
|
||||
"JSONStream": ["JSONStream@1.3.5", "", { "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" }, "bin": { "JSONStream": "./bin.js" } }, "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
||||
|
||||
"ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"array-ify": ["array-ify@1.0.0", "", {}, "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
|
||||
|
||||
"cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
|
||||
|
||||
"commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
|
||||
|
||||
"compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"conventional-changelog-angular": ["conventional-changelog-angular@7.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ=="],
|
||||
|
||||
"conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@7.0.2", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w=="],
|
||||
|
||||
"conventional-commits-parser": ["conventional-commits-parser@5.0.0", "", { "dependencies": { "JSONStream": "^1.3.5", "is-text-path": "^2.0.0", "meow": "^12.0.1", "split2": "^4.0.0" }, "bin": { "conventional-commits-parser": "cli.mjs" } }, "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA=="],
|
||||
|
||||
"cosmiconfig": ["cosmiconfig@9.0.1", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ=="],
|
||||
|
||||
"cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.3.0", "", { "dependencies": { "jiti": "2.6.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"dargs": ["dargs@8.1.0", "", {}, "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||
|
||||
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
|
||||
|
||||
"error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||
|
||||
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
||||
|
||||
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
|
||||
|
||||
"execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||
|
||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||
|
||||
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
|
||||
|
||||
"get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="],
|
||||
|
||||
"git-raw-commits": ["git-raw-commits@4.0.0", "", { "dependencies": { "dargs": "^8.0.0", "meow": "^12.0.1", "split2": "^4.0.0" }, "bin": { "git-raw-commits": "cli.mjs" } }, "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="],
|
||||
|
||||
"globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="],
|
||||
|
||||
"husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="],
|
||||
|
||||
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="],
|
||||
|
||||
"is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
|
||||
|
||||
"is-text-path": ["is-text-path@2.0.0", "", { "dependencies": { "text-extensions": "^2.0.0" } }, "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
|
||||
"jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||
|
||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||
|
||||
"lint-staged": ["lint-staged@15.5.2", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^13.1.0", "debug": "^4.4.0", "execa": "^8.0.1", "lilconfig": "^3.1.3", "listr2": "^8.2.5", "micromatch": "^4.0.8", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.7.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w=="],
|
||||
|
||||
"listr2": ["listr2@8.3.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
|
||||
|
||||
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
|
||||
|
||||
"lodash.kebabcase": ["lodash.kebabcase@4.1.1", "", {}, "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g=="],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"lodash.mergewith": ["lodash.mergewith@4.6.2", "", {}, "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="],
|
||||
|
||||
"lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
|
||||
|
||||
"lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="],
|
||||
|
||||
"lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="],
|
||||
|
||||
"lodash.upperfirst": ["lodash.upperfirst@4.3.1", "", {}, "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg=="],
|
||||
|
||||
"log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="],
|
||||
|
||||
"meow": ["meow@12.1.1", "", {}, "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw=="],
|
||||
|
||||
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="],
|
||||
|
||||
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="],
|
||||
|
||||
"onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
||||
|
||||
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
||||
|
||||
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||
|
||||
"semver": ["semver@7.8.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
|
||||
|
||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
"string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
|
||||
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"text-extensions": ["text-extensions@2.4.0", "", {}, "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g=="],
|
||||
|
||||
"through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||
|
||||
"unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||
|
||||
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"@commitlint/config-validator/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||
|
||||
"@commitlint/format/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"@commitlint/load/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"@commitlint/top-level/find-up": ["find-up@7.0.0", "", { "dependencies": { "locate-path": "^7.2.0", "path-exists": "^5.0.0", "unicorn-magic": "^0.1.0" } }, "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g=="],
|
||||
|
||||
"@commitlint/types/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"lint-staged/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
|
||||
|
||||
"log-update/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
|
||||
"slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
|
||||
|
||||
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"@commitlint/config-validator/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@commitlint/top-level/find-up/locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="],
|
||||
|
||||
"@commitlint/top-level/find-up/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
|
||||
|
||||
"cli-truncate/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"cli-truncate/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
|
||||
|
||||
"log-update/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"@commitlint/top-level/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="],
|
||||
|
||||
"cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"@commitlint/top-level/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="],
|
||||
|
||||
"@commitlint/top-level/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
# Menambahkan Aplikasi Baru ke Deployment
|
||||
|
||||
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.
|
||||
|
||||
## 1. Buat repo aplikasi
|
||||
|
||||
Buat repo baru di GitHub dengan pola nama:
|
||||
|
||||
```text
|
||||
https://github.com/asepharyana/asepharyana-hub-<nama-app>.git
|
||||
```
|
||||
|
||||
Lalu tambahkan ke root hub sebagai submodule:
|
||||
|
||||
```bash
|
||||
git submodule add https://github.com/asepharyana/asepharyana-hub-<nama-app>.git apps/<nama-app>
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
## 2. Tambahkan Dockerfile
|
||||
|
||||
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
|
||||
```
|
||||
@@ -0,0 +1,52 @@
|
||||
# ADR 0001: Use a Hub Repository with App Submodules
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The project contains multiple independent application services that share one deployment surface: Docker Compose, Traefik routing, GitHub Actions workflows, and operational documentation.
|
||||
|
||||
The services should be developed and versioned independently, while deployment infrastructure should remain centralized so production routing and compose manifests stay consistent.
|
||||
|
||||
## Decision
|
||||
|
||||
Use `asepharyana-hub` as the root hub repository.
|
||||
|
||||
- Application code lives under `apps/<service>` as Git submodules.
|
||||
- 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:
|
||||
|
||||
| Service | Path | Remote |
|
||||
| -------------- | ---------------- | --------------------------------------- |
|
||||
| Elysia API | `apps/elysia` | `asepharyana/asepharyana-hub-elysia` |
|
||||
| React frontend | `apps/react` | `asepharyana/asepharyana-hub-react` |
|
||||
| Rust auth API | `apps/rust-auth` | `asepharyana/asepharyana-hub-rust-auth` |
|
||||
| Scraper API | `apps/scraper` | `asepharyana/asepharyana-hub-scraper` |
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Each app can evolve in its own repository.
|
||||
- The hub pins exact submodule revisions for reproducible deployments.
|
||||
- Deployment infrastructure remains centralized and easier to audit.
|
||||
- Root tooling stays lightweight and does not impose one build system on every service.
|
||||
|
||||
### Negative
|
||||
|
||||
- Developers must understand Git submodule workflows.
|
||||
- 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,23 @@
|
||||
# Architecture Decision Records
|
||||
|
||||
This directory contains Architecture Decision Records (ADRs).
|
||||
|
||||
Each ADR documents an architectural decision, its context, and its consequences.
|
||||
|
||||
## How to use
|
||||
|
||||
```bash
|
||||
coder-workflow adr new "<decision title>"
|
||||
coder-workflow adr list
|
||||
coder-workflow adr status <id> --status accepted
|
||||
coder-workflow adr graph
|
||||
```
|
||||
|
||||
## Status Definitions
|
||||
|
||||
| Status | Meaning |
|
||||
| ---------- | --------------------------- |
|
||||
| Proposed | Under discussion |
|
||||
| Accepted | Agreed upon and implemented |
|
||||
| Deprecated | No longer recommended |
|
||||
| Superseded | Replaced by a newer ADR |
|
||||
@@ -0,0 +1,39 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,686 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,712 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,533 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,29 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,130 @@
|
||||
# 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
|
||||
@@ -0,0 +1,197 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,21 @@
|
||||
# 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`
|
||||
@@ -0,0 +1,100 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,135 @@
|
||||
# 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,10 @@
|
||||
import antfu from '@antfu/eslint-config';
|
||||
|
||||
export default antfu({
|
||||
formatters: true,
|
||||
typescript: true,
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'eslint-comments/no-unlimited-disable': 'off',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
# Infrastructure
|
||||
|
||||
Docker Compose and Traefik configuration for `asepharyana-hub`.
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
infra/
|
||||
├── compose/ # One compose file per stack/service
|
||||
│ ├── traefik.yml # Public reverse proxy
|
||||
│ ├── shared.yml # Shared Redis
|
||||
│ ├── react.yml # React SPA
|
||||
│ ├── scraper.yml # Scraper API
|
||||
│ ├── elysia.yml # Elysia API
|
||||
│ └── rust-auth.yml # Rust auth API
|
||||
├── 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
|
||||
```
|
||||
|
||||
Archived configs that are not deployed live under `docs/config/`.
|
||||
|
||||
## First-time setup
|
||||
|
||||
Create the shared Docker network before starting any service:
|
||||
|
||||
```bash
|
||||
docker network create app-shared-net
|
||||
```
|
||||
|
||||
Create `.env` from `.env.example` and fill production values. Do not commit `.env`.
|
||||
|
||||
## Deployment order
|
||||
|
||||
The GitHub deploy workflow combines the active compose files automatically. For manual deployment, use this order:
|
||||
|
||||
```bash
|
||||
docker compose -f infra/compose/shared.yml up -d
|
||||
docker compose -f infra/compose/traefik.yml up -d
|
||||
|
||||
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
|
||||
```
|
||||
@@ -0,0 +1,24 @@
|
||||
services:
|
||||
elysia-api:
|
||||
container_name: elysia-api
|
||||
image: ghcr.io/asepharyana/asepharyana-hub/elysia-api:sha-0899edc
|
||||
restart: always
|
||||
networks:
|
||||
app-shared-net:
|
||||
aliases:
|
||||
- elysia-api
|
||||
env_file:
|
||||
- ../../.env
|
||||
environment:
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- JWT_SECRET=${JWT_SECRET:?JWT_SECRET is required}
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- GITHUB_TOKEN=${GITHUB_TOKEN}
|
||||
- PORT=4092
|
||||
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otlp-metrics-backend:4317
|
||||
- OTEL_SERVICE_NAME=elysia-api
|
||||
|
||||
networks:
|
||||
app-shared-net:
|
||||
name: app-shared-net
|
||||
external: true
|
||||
@@ -0,0 +1,13 @@
|
||||
services:
|
||||
react-web:
|
||||
container_name: react-web
|
||||
image: ghcr.io/asepharyana/asepharyana-hub/react-web:sha-0899edc
|
||||
restart: always
|
||||
networks: [app-shared-net]
|
||||
environment:
|
||||
- VITE_API_URL=https://scraper.asepharyana.my.id/api
|
||||
- VITE_ELYSIA_URL=https://elysia.asepharyana.my.id
|
||||
networks:
|
||||
app-shared-net:
|
||||
name: app-shared-net
|
||||
external: true
|
||||
@@ -0,0 +1,20 @@
|
||||
services:
|
||||
rust-auth:
|
||||
container_name: rust-auth-api
|
||||
image: ghcr.io/asepharyana/asepharyana-hub/rust-auth:sha-0899edc
|
||||
restart: always
|
||||
networks:
|
||||
app-shared-net:
|
||||
aliases:
|
||||
- rust-auth
|
||||
env_file:
|
||||
- ../../.env
|
||||
environment:
|
||||
- PORT=3000
|
||||
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otlp-metrics-backend:4317
|
||||
- OTEL_SERVICE_NAME=rust-auth
|
||||
|
||||
networks:
|
||||
app-shared-net:
|
||||
name: app-shared-net
|
||||
external: true
|
||||
@@ -0,0 +1,21 @@
|
||||
services:
|
||||
scraper-api:
|
||||
container_name: scraper-api
|
||||
image: ghcr.io/asepharyana/asepharyana-hub/scraper-api:sha-0899edc
|
||||
restart: always
|
||||
networks:
|
||||
app-shared-net:
|
||||
aliases:
|
||||
- scraper-api
|
||||
env_file:
|
||||
- ../../.env
|
||||
environment:
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- JWT_SECRET=${JWT_SECRET:?JWT_SECRET is required}
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otlp-metrics-backend:4317
|
||||
- OTEL_SERVICE_NAME=scraper-api
|
||||
networks:
|
||||
app-shared-net:
|
||||
name: app-shared-net
|
||||
external: true
|
||||
@@ -0,0 +1,21 @@
|
||||
services:
|
||||
redis:
|
||||
container_name: redis
|
||||
image: 'redis:alpine'
|
||||
restart: always
|
||||
networks:
|
||||
app-shared-net:
|
||||
aliases:
|
||||
- redis
|
||||
ports:
|
||||
- '${SHARED_REDIS_EXPOSE:-127.0.0.1:6379:6379}'
|
||||
volumes:
|
||||
- 'redis_data:/data'
|
||||
|
||||
networks:
|
||||
app-shared-net:
|
||||
name: app-shared-net
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
redis_data: null
|
||||
@@ -0,0 +1,74 @@
|
||||
services:
|
||||
traefik:
|
||||
container_name: traefik
|
||||
image: traefik:v3.6
|
||||
restart: always
|
||||
sysctls:
|
||||
- net.core.somaxconn=65535
|
||||
- net.ipv4.ip_local_port_range=1024 65535
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 1048576
|
||||
hard: 1048576
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
networks:
|
||||
- app-shared-net
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:10.0.1.1'
|
||||
command:
|
||||
- '--api.dashboard=true'
|
||||
- '--api.insecure=false'
|
||||
- '--providers.docker=true'
|
||||
- '--providers.docker.endpoint=unix:///var/run/docker.sock'
|
||||
- '--providers.docker.exposedByDefault=false'
|
||||
- '--providers.docker.network=app-shared-net'
|
||||
- '--providers.docker.watch=true'
|
||||
- '--providers.file.directory=/etc/traefik/dynamic'
|
||||
- '--providers.file.watch=true'
|
||||
- '--entryPoints.web.address=:80'
|
||||
- '--entryPoints.web.http.redirections.entryPoint.to=websecure'
|
||||
- '--entryPoints.web.http.redirections.entryPoint.scheme=https'
|
||||
- '--accesslog=true'
|
||||
- '--accesslog.bufferingsize=100'
|
||||
- '--log.level=INFO'
|
||||
- '--log.format=json'
|
||||
- '--entryPoints.web.transport.respondingTimeouts.readTimeout=0'
|
||||
- '--entryPoints.web.transport.respondingTimeouts.writeTimeout=0'
|
||||
- '--entryPoints.web.transport.respondingTimeouts.idleTimeout=0'
|
||||
- '--entryPoints.web.transport.lifeCycle.requestAcceptGraceTimeout=15s'
|
||||
- '--entryPoints.web.transport.lifeCycle.graceTimeOut=10s'
|
||||
- '--entryPoints.websecure.transport.respondingTimeouts.readTimeout=0'
|
||||
- '--entryPoints.websecure.transport.respondingTimeouts.writeTimeout=0'
|
||||
- '--entryPoints.websecure.transport.respondingTimeouts.idleTimeout=0'
|
||||
- '--entryPoints.websecure.transport.lifeCycle.requestAcceptGraceTimeout=15s'
|
||||
- '--entryPoints.websecure.transport.lifeCycle.graceTimeOut=10s'
|
||||
- '--entryPoints.websecure.address=:443'
|
||||
- '--experimental.plugins.real-ip.moduleName=github.com/soulbalz/traefik-real-ip'
|
||||
- '--experimental.plugins.real-ip.version=v1.0.3'
|
||||
- '--experimental.plugins.blockpath.moduleName=github.com/traefik/plugin-blockpath'
|
||||
- '--experimental.plugins.blockpath.version=v0.2.1'
|
||||
environment:
|
||||
- DOCKER_API_VERSION=1.41
|
||||
- GOMEMLIMIT=4096MiB
|
||||
- GOGC=200
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ${TRAEFIK_CONFIG_PATH:-/root/asepharyana-hub/infra/traefik/dynamic}:/etc/traefik/dynamic:ro
|
||||
- ${TRAEFIK_CERT_ASEPHARYANA_MY_ID_PEM:-/root/asepharyana.my.id.pem}:/etc/traefik/certs/asepharyana.my.id.pem:ro
|
||||
- ${TRAEFIK_CERT_ASEPHARYANA_MY_ID_KEY:-/root/asepharyana.my.id.key}:/etc/traefik/certs/asepharyana.my.id.key:ro
|
||||
- ${TRAEFIK_CERT_ASEPHARYANA_WEB_ID_PEM:-/root/asepharyana.web.id.pem}:/etc/traefik/certs/asepharyana.web.id.pem:ro
|
||||
- ${TRAEFIK_CERT_ASEPHARYANA_WEB_ID_KEY:-/root/asepharyana.web.id.key}:/etc/traefik/certs/asepharyana.web.id.key:ro
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.traefik.rule=Host(`traefik.asepharyana.my.id`) || Host(`traefik.asepharyana.web.id`)'
|
||||
- 'traefik.http.routers.traefik.service=api@internal'
|
||||
- 'traefik.http.routers.traefik.entrypoints=websecure'
|
||||
- 'traefik.http.routers.traefik.tls=true'
|
||||
- 'traefik.http.routers.traefik.middlewares=admin-chain@file'
|
||||
|
||||
networks:
|
||||
app-shared-net:
|
||||
name: app-shared-net
|
||||
external: true
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Create database if not exists
|
||||
CREATE DATABASE IF NOT EXISTS `tracer_study`;
|
||||
|
||||
-- Create dedicated user for tracer_study
|
||||
CREATE USER IF NOT EXISTS 'tracerstudy'@'%' IDENTIFIED BY 'tracerstudy_secret';
|
||||
GRANT ALL PRIVILEGES ON `tracer_study`.* TO 'tracerstudy'@'%';
|
||||
|
||||
|
||||
-- Flush privileges
|
||||
FLUSH PRIVILEGES;
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,26 @@
|
||||
# build stage
|
||||
FROM oven/bun:1-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# install dependencies with cache mounts
|
||||
COPY apps/elysia/package.json apps/elysia/bun.lock ./
|
||||
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
||||
bun install --frozen-lockfile
|
||||
|
||||
# build the application
|
||||
COPY apps/elysia ./
|
||||
RUN bun run build
|
||||
|
||||
# runtime stage
|
||||
FROM oven/bun:1-distroless
|
||||
WORKDIR /app
|
||||
|
||||
# copy build artifacts
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/package.json ./
|
||||
|
||||
# distroless uses nonroot user (UID 65532) by default, or we can use it
|
||||
USER nonroot
|
||||
|
||||
EXPOSE 4092
|
||||
CMD ["run", "dist/index.js"]
|
||||
Vendored
+75
@@ -0,0 +1,75 @@
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
import { resolve, sep } from "node:path"
|
||||
|
||||
const distDir = resolve("./dist")
|
||||
const indexPath = resolve(distDir, "index.html")
|
||||
|
||||
const contentTypes = {
|
||||
html: "text/html; charset=utf-8",
|
||||
css: "text/css; charset=utf-8",
|
||||
js: "application/javascript; charset=utf-8",
|
||||
json: "application/json; charset=utf-8",
|
||||
svg: "image/svg+xml",
|
||||
png: "image/png",
|
||||
jpg: "image/jpeg",
|
||||
jpeg: "image/jpeg",
|
||||
gif: "image/gif",
|
||||
webp: "image/webp",
|
||||
ico: "image/x-icon",
|
||||
woff: "font/woff",
|
||||
woff2: "font/woff2",
|
||||
ttf: "font/ttf",
|
||||
}
|
||||
|
||||
function responseFromFile(filePath, headers) {
|
||||
try {
|
||||
return new Response(readFileSync(filePath), { headers })
|
||||
} catch (error) {
|
||||
if (error?.code === "ENOENT") {
|
||||
return new Response("Not Found", { status: 404 })
|
||||
}
|
||||
|
||||
console.error("File read error:", error)
|
||||
return new Response("Internal Server Error", { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
function serveIndex() {
|
||||
return responseFromFile(indexPath, {
|
||||
"Content-Type": contentTypes.html,
|
||||
"Cache-Control": "no-cache",
|
||||
})
|
||||
}
|
||||
|
||||
function serveFile(filePath, pathname) {
|
||||
const ext = filePath.split(".").pop() || ""
|
||||
return responseFromFile(filePath, {
|
||||
"Content-Type": contentTypes[ext] || "application/octet-stream",
|
||||
"Cache-Control": pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache",
|
||||
})
|
||||
}
|
||||
|
||||
Bun.serve({
|
||||
port: 80,
|
||||
hostname: "0.0.0.0",
|
||||
fetch(req) {
|
||||
const url = new URL(req.url)
|
||||
const pathname = url.pathname
|
||||
const filePath = resolve(distDir, pathname.slice(1))
|
||||
const insideDist = filePath === distDir || filePath.startsWith(`${distDir}${sep}`)
|
||||
|
||||
if (!insideDist) {
|
||||
return new Response("Forbidden", { status: 403 })
|
||||
}
|
||||
|
||||
if (pathname !== "/" && existsSync(filePath)) {
|
||||
return serveFile(filePath, pathname)
|
||||
}
|
||||
|
||||
if (!pathname.includes(".") && existsSync(indexPath)) {
|
||||
return serveIndex()
|
||||
}
|
||||
|
||||
return new Response("Not Found", { status: 404 })
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
# ─── Stage 1: Build ─────────────────────────────────────────────────────────
|
||||
FROM oven/bun:1 AS builder
|
||||
WORKDIR /app
|
||||
COPY apps/react/package.json apps/react/bun.lock ./
|
||||
RUN bun install --frozen-lockfile
|
||||
COPY apps/react .
|
||||
RUN bun run build
|
||||
|
||||
# ─── Stage 2: Runtime (Bun static server) ──────────────────────────────────
|
||||
FROM oven/bun:1-alpine
|
||||
WORKDIR /app
|
||||
COPY infra/docker/react-server.js ./server.js
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
EXPOSE 80
|
||||
CMD ["bun", "server.js"]
|
||||
@@ -0,0 +1,42 @@
|
||||
# Use cargo-chef for dependency caching
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-1.89.0 AS chef
|
||||
WORKDIR /app
|
||||
|
||||
FROM chef AS planner
|
||||
COPY apps/rust-auth .
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
FROM chef AS builder
|
||||
COPY --from=planner /app/recipe.json recipe.json
|
||||
# Utilize buildkit cache mounts for cargo
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/app/target \
|
||||
cargo chef cook --release --recipe-path recipe.json
|
||||
|
||||
# Build application
|
||||
COPY apps/rust-auth .
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/app/target \
|
||||
cargo build --release && \
|
||||
cp target/release/rust-auth /app/rust-auth
|
||||
|
||||
# Final runtime image
|
||||
FROM debian:bookworm-slim AS runtime
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
libssl3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add non-root user
|
||||
RUN groupadd -g 1001 appgroup && \
|
||||
useradd -u 1001 -g appgroup -s /bin/sh appuser
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/rust-auth /app/rust-auth
|
||||
|
||||
# Run as non-root
|
||||
USER appuser
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["./rust-auth"]
|
||||
@@ -0,0 +1,50 @@
|
||||
# Use cargo-chef for dependency caching
|
||||
FROM lukemathwalker/cargo-chef:latest-rust-1.89.0 AS chef
|
||||
WORKDIR /app
|
||||
|
||||
# Install Node.js if needed for build scripts
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
FROM chef AS planner
|
||||
COPY apps/scraper .
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
FROM chef AS builder
|
||||
COPY --from=planner /app/recipe.json recipe.json
|
||||
# Utilize buildkit cache mounts for cargo
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/app/target \
|
||||
cargo chef cook --release --recipe-path recipe.json
|
||||
|
||||
# Build application
|
||||
COPY apps/scraper .
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/app/target \
|
||||
cargo build --release && \
|
||||
cp target/release/scraper /app/scraper
|
||||
|
||||
# Final runtime image
|
||||
FROM debian:bookworm-slim AS runtime
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
libssl3 \
|
||||
chromium \
|
||||
fonts-liberation \
|
||||
fonts-noto-color-emoji \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add non-root user
|
||||
RUN groupadd -g 1001 appgroup && \
|
||||
useradd -u 1001 -g appgroup -s /bin/sh appuser
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/scraper /app/scraper
|
||||
|
||||
# Run as non-root
|
||||
USER appuser
|
||||
|
||||
EXPOSE 4091
|
||||
CMD ["./scraper"]
|
||||
@@ -0,0 +1,61 @@
|
||||
# Traefik Environment Configuration
|
||||
|
||||
This document describes environment variables used to configure Traefik certificate and config paths in production deployments.
|
||||
|
||||
## Certificate Path Environment Variables
|
||||
|
||||
All certificate paths support environment variable substitution with safe fallback defaults. This allows flexible certificate management across different deployment environments without modifying compose files.
|
||||
|
||||
### Configuration Variables
|
||||
|
||||
| Variable | Description | Default Path | Purpose |
|
||||
| ------------------------------------- | --------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------- |
|
||||
| `TRAEFIK_CONFIG_PATH` | Directory containing dynamic Traefik configuration files (YAML) | `/root/asepharyana-hub/infra/traefik/dynamic` | Location of middleware, router, and service definitions |
|
||||
| `TRAEFIK_CERT_ASEPHARYANA_MY_ID_PEM` | Certificate file for asepharyana.my.id | `/root/asepharyana.my.id.pem` | SSL/TLS certificate for asepharyana.my.id domain |
|
||||
| `TRAEFIK_CERT_ASEPHARYANA_MY_ID_KEY` | Key file for asepharyana.my.id | `/root/asepharyana.my.id.key` | SSL/TLS private key for asepharyana.my.id domain |
|
||||
| `TRAEFIK_CERT_ASEPHARYANA_WEB_ID_PEM` | Certificate file for asepharyana.web.id | `/root/asepharyana.web.id.pem` | SSL/TLS certificate for asepharyana.web.id domain |
|
||||
| `TRAEFIK_CERT_ASEPHARYANA_WEB_ID_KEY` | Key file for asepharyana.web.id | `/root/asepharyana.web.id.key` | SSL/TLS private key for asepharyana.web.id domain |
|
||||
|
||||
## Usage
|
||||
|
||||
### Default Behavior (Production)
|
||||
|
||||
If no environment variables are set, Traefik will use the default paths shown above. This is suitable for production deployments where certificates are installed at these standard locations.
|
||||
|
||||
```bash
|
||||
docker compose -f infra/compose/traefik.yml up -d
|
||||
```
|
||||
|
||||
### Custom Paths (Custom Deployments)
|
||||
|
||||
To override paths for a custom deployment, set environment variables before starting services:
|
||||
|
||||
```bash
|
||||
export TRAEFIK_CONFIG_PATH=/etc/traefik/custom-dynamic
|
||||
|
||||
docker compose -f infra/compose/traefik.yml up -d
|
||||
```
|
||||
|
||||
### Via .env File
|
||||
|
||||
Create or update your `.env` file in the deployment directory:
|
||||
|
||||
```env
|
||||
TRAEFIK_CONFIG_PATH=/root/asepharyana-hub/infra/traefik/dynamic
|
||||
TRAEFIK_CERT_ASEPHARYANA_MY_ID_PEM=/root/asepharyana.my.id.pem
|
||||
TRAEFIK_CERT_ASEPHARYANA_MY_ID_KEY=/root/asepharyana.my.id.key
|
||||
TRAEFIK_CERT_ASEPHARYANA_WEB_ID_PEM=/root/asepharyana.web.id.pem
|
||||
TRAEFIK_CERT_ASEPHARYANA_WEB_ID_KEY=/root/asepharyana.web.id.key
|
||||
```
|
||||
|
||||
Then deploy:
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env -f infra/compose/traefik.yml up -d
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- All certificate paths use read-only mounts (`:ro`) for security
|
||||
- If a certificate file is missing at the specified path, Docker volume mounting will fail—ensure certificates exist before starting Traefik
|
||||
- The dynamic configuration directory must contain valid YAML files for Traefik to load properly
|
||||
@@ -0,0 +1,57 @@
|
||||
http:
|
||||
routers:
|
||||
# ── React SPA (domain root) ──
|
||||
react:
|
||||
rule: 'Host(`asepharyana.my.id`) || Host(`asepharyana.web.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
service: react-service
|
||||
|
||||
# ── Scraper API ──
|
||||
scraper:
|
||||
rule: 'Host(`scraper.asepharyana.my.id`) || Host(`api.asepharyana.my.id`) || Host(`scraper.asepharyana.web.id`) || Host(`api.asepharyana.web.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- common-chain@file
|
||||
service: scraper-service
|
||||
|
||||
# ── Elysia API ──
|
||||
elysia:
|
||||
rule: 'Host(`elysia.asepharyana.my.id`) || Host(`elysia.asepharyana.web.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- common-chain@file
|
||||
service: elysia-service
|
||||
|
||||
# ── Rust Auth API ──
|
||||
rust-auth:
|
||||
rule: 'Host(`auth.asepharyana.my.id`) || Host(`auth.asepharyana.web.id`)'
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
middlewares:
|
||||
- common-chain@file
|
||||
service: rust-auth-service
|
||||
|
||||
services:
|
||||
react-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://react-web:80'
|
||||
scraper-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://scraper-api:4091'
|
||||
elysia-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://elysia-api:4092'
|
||||
rust-auth-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: 'http://rust-auth:3000'
|
||||
@@ -0,0 +1,74 @@
|
||||
http:
|
||||
middlewares:
|
||||
secure-headers:
|
||||
headers:
|
||||
sslRedirect: true
|
||||
forceSTSHeader: true
|
||||
stsSeconds: 31536000
|
||||
stsIncludeSubdomains: true
|
||||
stsPreload: true
|
||||
frameDeny: true
|
||||
contentTypeNosniff: true
|
||||
browserXSSFilter: true
|
||||
referrerPolicy: 'same-origin'
|
||||
customResponseHeaders:
|
||||
X-Content-Type-Options: 'nosniff'
|
||||
X-Frame-Options: 'DENY'
|
||||
X-XSS-Protection: '1; mode=block'
|
||||
Referrer-Policy: 'same-origin'
|
||||
Permissions-Policy: 'geolocation=(), microphone=(), camera=()'
|
||||
compress:
|
||||
compress:
|
||||
minResponseBodyBytes: 256
|
||||
excludedContentTypes:
|
||||
- 'image/*'
|
||||
- 'application/octet-stream'
|
||||
retry:
|
||||
retry:
|
||||
attempts: 3
|
||||
rate-limit:
|
||||
rateLimit:
|
||||
average: 100
|
||||
burst: 50
|
||||
buffer:
|
||||
buffering:
|
||||
maxRequestBodyBytes: 10485760
|
||||
maxResponseBodyBytes: 10485760
|
||||
memRequestBodyBytes: 1048576
|
||||
memResponseBodyBytes: 1048576
|
||||
admin-chain:
|
||||
chain:
|
||||
middlewares:
|
||||
- secure-headers
|
||||
- compress
|
||||
- retry
|
||||
|
||||
# ── Useful Plugins ──
|
||||
real-ip:
|
||||
plugin:
|
||||
real-ip:
|
||||
excludednetworks:
|
||||
- '127.0.0.1/32'
|
||||
realipheader: 'CF-Connecting-IP'
|
||||
|
||||
block-sensitive-paths:
|
||||
plugin:
|
||||
blockpath:
|
||||
regex:
|
||||
- "^/\\.env"
|
||||
- "^/\\.git"
|
||||
- '^/wp-admin'
|
||||
- "^/wp-login\\.php"
|
||||
- "^/config\\.php"
|
||||
|
||||
# ── Common Chain ──
|
||||
common-chain:
|
||||
chain:
|
||||
middlewares:
|
||||
# - real-ip
|
||||
# - block-sensitive-paths
|
||||
- secure-headers
|
||||
- compress
|
||||
- retry
|
||||
- rate-limit
|
||||
- buffer
|
||||
@@ -0,0 +1,14 @@
|
||||
tls:
|
||||
certificates:
|
||||
# Legacy production layout: asephstech.pem is paired with asephscloud.key.
|
||||
- certFile: /etc/traefik/certs/asephstech.pem
|
||||
keyFile: /etc/traefik/certs/asephscloud.key
|
||||
- certFile: /etc/traefik/certs/asepharyana.my.id.pem
|
||||
keyFile: /etc/traefik/certs/asepharyana.my.id.key
|
||||
- certFile: /etc/traefik/certs/asepharyana.web.id.pem
|
||||
keyFile: /etc/traefik/certs/asepharyana.web.id.key
|
||||
stores:
|
||||
default:
|
||||
defaultCertificate:
|
||||
certFile: /etc/traefik/certs/asephstech.pem
|
||||
keyFile: /etc/traefik/certs/asephscloud.key
|
||||
@@ -0,0 +1,29 @@
|
||||
api:
|
||||
dashboard: true
|
||||
insecure: true
|
||||
|
||||
log:
|
||||
level: INFO
|
||||
format: json
|
||||
|
||||
accessLog: {}
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
address: ':80'
|
||||
http:
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: websecure
|
||||
scheme: https
|
||||
websecure:
|
||||
address: ':443'
|
||||
|
||||
providers:
|
||||
docker:
|
||||
endpoint: 'unix:///var/run/docker.sock'
|
||||
exposedByDefault: false
|
||||
network: app-shared-net
|
||||
file:
|
||||
directory: /etc/traefik/dynamic
|
||||
watch: true
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "asepharyana-hub",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write .",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.4.2",
|
||||
"eslint": "^9.18.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"git-submodules": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/nix/store/a1vm413z2gayp4dfj6fvcnsp880bx08g-apps-leptos-0.1.0
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# Copy .env from root to all direct subprojects/packages (not nested, not build, not .git, not .github, not node_modules, not .turbo, not .next, not .vscode, not dist, not public, not src, not coverage, not logs, not .devcontainer, not .yarn, not target)
|
||||
# Do NOT copy to /apps or /packages root, only to their subfolders.
|
||||
|
||||
ROOT_ENV="./.env"
|
||||
|
||||
if [ ! -f "$ROOT_ENV" ]; then
|
||||
echo "Root .env file not found at $ROOT_ENV"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for parent in apps packages; do
|
||||
for dir in ./$parent/*/; do
|
||||
# Remove trailing slash
|
||||
dir="${dir%/}"
|
||||
base=$(basename "$dir")
|
||||
if [[ "$base" =~ ^(\.git|\.github|node_modules|\.turbo|\.next|\.vscode|dist|public|src|coverage|logs|\.devcontainer|\.yarn|target)$ ]]; then
|
||||
continue
|
||||
fi
|
||||
cp "$ROOT_ENV" "$dir/.env"
|
||||
echo "Copied .env to $dir/.env"
|
||||
done
|
||||
done
|
||||
@@ -0,0 +1,169 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Clean all node_modules directories in the repository with optional cache and lockfile cleanup.
|
||||
|
||||
.DESCRIPTION
|
||||
This script recursively finds and removes all 'node_modules' directories starting at the repo root.
|
||||
Optionally, it can also remove build caches and lockfiles, and run 'pnpm store prune'.
|
||||
|
||||
.PARAMETER IncludeCache
|
||||
Also remove common cache/build output directories (e.g., .next, .turbo, .vite, node_modules/.cache, dist, build, coverage, out, storybook-static).
|
||||
|
||||
.PARAMETER IncludeLock
|
||||
Also remove lock files (pnpm-lock.yaml, package-lock.json, yarn.lock) in the repo.
|
||||
|
||||
.PARAMETER PruneStore
|
||||
After deletion, try to run 'pnpm store prune' if pnpm is installed.
|
||||
|
||||
.PARAMETER Yes
|
||||
Proceed without interactive confirmation (non-interactive mode).
|
||||
|
||||
.PARAMETER DryRun
|
||||
Show what would be removed without deleting anything.
|
||||
|
||||
.EXAMPLE
|
||||
# Preview what will be removed
|
||||
./scripts/clean-node-modules.ps1 -DryRun
|
||||
|
||||
.EXAMPLE
|
||||
# Clean node_modules only, no prompt
|
||||
./scripts/clean-node-modules.ps1 -Yes
|
||||
|
||||
.EXAMPLE
|
||||
# Deep clean including caches and lockfiles, and prune pnpm store
|
||||
./scripts/clean-node-modules.ps1 -IncludeCache -IncludeLock -PruneStore -Yes
|
||||
#>
|
||||
|
||||
[CmdletBinding(SupportsShouldProcess=$true)]
|
||||
param(
|
||||
[switch] $IncludeCache,
|
||||
[switch] $IncludeLock,
|
||||
[switch] $PruneStore,
|
||||
[switch] $Yes,
|
||||
[switch] $DryRun
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Write-Section($text) {
|
||||
Write-Host "`n==== $text ====\n" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Safe-RemoveDirectory {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string] $Path,
|
||||
[switch] $Preview
|
||||
)
|
||||
if (-not (Test-Path -LiteralPath $Path)) { return }
|
||||
if ($Preview) { Write-Host "[dir] $Path"; return }
|
||||
try {
|
||||
# Use cmd rmdir for better handling of read-only/long paths on Windows PowerShell 5.1
|
||||
& cmd.exe /c "rmdir /s /q \"$Path\"" | Out-Null
|
||||
} catch {
|
||||
try { Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop } catch {
|
||||
Write-Warning "Failed to remove directory: $Path -> $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Safe-RemoveFile {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string] $Path,
|
||||
[switch] $Preview
|
||||
)
|
||||
if (-not (Test-Path -LiteralPath $Path)) { return }
|
||||
if ($Preview) { Write-Host "[file] $Path"; return }
|
||||
try { Remove-Item -LiteralPath $Path -Force -ErrorAction Stop } catch {
|
||||
Write-Warning "Failed to remove file: $Path -> $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# Resolve repo root (this script lives in ./scripts)
|
||||
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||
Set-Location -LiteralPath $RepoRoot
|
||||
|
||||
Write-Host "Repo root: $RepoRoot" -ForegroundColor DarkGray
|
||||
|
||||
# Accumulators
|
||||
$DirsToDelete = New-Object System.Collections.Generic.List[string]
|
||||
$FilesToDelete = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
# 1) node_modules everywhere (including root)
|
||||
Write-Section 'Scanning node_modules directories'
|
||||
$nodeModulesDirs = Get-ChildItem -LiteralPath $RepoRoot -Directory -Recurse -Force -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -eq 'node_modules' }
|
||||
|
||||
# Ensure root node_modules is included if present
|
||||
$rootNM = Join-Path $RepoRoot 'node_modules'
|
||||
if (Test-Path -LiteralPath $rootNM) {
|
||||
$DirsToDelete.Add($rootNM)
|
||||
}
|
||||
foreach ($d in $nodeModulesDirs) {
|
||||
if (-not $DirsToDelete.Contains($d.FullName)) { $DirsToDelete.Add($d.FullName) }
|
||||
}
|
||||
Write-Host ("Found {0} node_modules directory(ies)" -f $DirsToDelete.Count)
|
||||
|
||||
# 2) Optional caches/output folders
|
||||
if ($IncludeCache) {
|
||||
Write-Section 'Scanning cache/output directories'
|
||||
$cacheNames = @(
|
||||
'.next', '.turbo', '.vite', '.parcel-cache', '.cache',
|
||||
'dist', 'build', 'coverage', 'out', 'storybook-static',
|
||||
'.wrangler'
|
||||
)
|
||||
# node_modules/.cache is common; include it via name match too
|
||||
$allDirs = Get-ChildItem -LiteralPath $RepoRoot -Directory -Recurse -Force -ErrorAction SilentlyContinue
|
||||
foreach ($dir in $allDirs) {
|
||||
if ($cacheNames -contains $dir.Name) {
|
||||
if (-not $DirsToDelete.Contains($dir.FullName)) { $DirsToDelete.Add($dir.FullName) }
|
||||
}
|
||||
}
|
||||
Write-Host ("Found {0} cache/output directory(ies)" -f ($DirsToDelete | Where-Object { Test-Path $_ }).Count)
|
||||
}
|
||||
|
||||
# 3) Optional lockfiles
|
||||
if ($IncludeLock) {
|
||||
Write-Section 'Scanning lock files'
|
||||
$lockGlobs = @('pnpm-lock.yaml', 'package-lock.json', 'yarn.lock')
|
||||
foreach ($glob in $lockGlobs) {
|
||||
$files = Get-ChildItem -LiteralPath $RepoRoot -Recurse -Force -File -Filter $glob -ErrorAction SilentlyContinue
|
||||
foreach ($f in $files) { if (-not $FilesToDelete.Contains($f.FullName)) { $FilesToDelete.Add($f.FullName) } }
|
||||
}
|
||||
Write-Host ("Found {0} lock file(s)" -f $FilesToDelete.Count)
|
||||
}
|
||||
|
||||
# 4) Summary
|
||||
Write-Section 'Summary'
|
||||
Write-Host ("Directories to delete: {0}" -f $DirsToDelete.Count)
|
||||
Write-Host ("Files to delete: {0}" -f $FilesToDelete.Count)
|
||||
|
||||
$preview = $DryRun -or (-not $Yes)
|
||||
if ($preview) {
|
||||
Write-Host "Preview mode (no deletions). Use -Yes to confirm, or pass -DryRun:$false to hide this list." -ForegroundColor Yellow
|
||||
foreach ($dir in $DirsToDelete) { Safe-RemoveDirectory -Path $dir -Preview }
|
||||
foreach ($fil in $FilesToDelete) { Safe-RemoveFile -Path $fil -Preview }
|
||||
if (-not $Yes) { Write-Host "\nRun again with -Yes to confirm deletion." -ForegroundColor Yellow }
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 5) Deletion
|
||||
Write-Section 'Deleting directories'
|
||||
foreach ($dir in $DirsToDelete) { Safe-RemoveDirectory -Path $dir }
|
||||
|
||||
if ($FilesToDelete.Count -gt 0) {
|
||||
Write-Section 'Deleting files'
|
||||
foreach ($fil in $FilesToDelete) { Safe-RemoveFile -Path $fil }
|
||||
}
|
||||
|
||||
# 6) Optional pnpm store prune
|
||||
if ($PruneStore) {
|
||||
Write-Section 'Pruning pnpm store'
|
||||
$pnpm = Get-Command pnpm -ErrorAction SilentlyContinue
|
||||
if ($null -ne $pnpm) {
|
||||
try { & pnpm store prune } catch { Write-Warning "pnpm store prune failed: $($_.Exception.Message)" }
|
||||
} else {
|
||||
Write-Warning "pnpm not found on PATH; skipping 'pnpm store prune'."
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "\nDone." -ForegroundColor Green
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Clean all node_modules directories with optional cache & lockfile cleanup.
|
||||
# Usage:
|
||||
# ./scripts/clean-node-modules.sh [--include-cache] [--include-lock] [--prune-store] [--yes] [--dry-run]
|
||||
|
||||
INCLUDE_CACHE=false
|
||||
INCLUDE_LOCK=false
|
||||
PRUNE_STORE=false
|
||||
YES=false
|
||||
DRY_RUN=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--include-cache) INCLUDE_CACHE=true ;;
|
||||
--include-lock) INCLUDE_LOCK=true ;;
|
||||
--prune-store) PRUNE_STORE=true ;;
|
||||
--yes) YES=true ;;
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
*) echo "Unknown option: $arg"; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
preview() {
|
||||
if [[ "$DRY_RUN" == true || "$YES" == false ]]; then echo 1; else echo 0; fi
|
||||
}
|
||||
|
||||
section() { echo -e "\n==== $* ====\n"; }
|
||||
|
||||
DIRS_TO_DELETE=()
|
||||
FILES_TO_DELETE=()
|
||||
|
||||
section "Scanning node_modules directories"
|
||||
while IFS= read -r -d '' d; do DIRS_TO_DELETE+=("$d"); done < <(find "$REPO_ROOT" -type d -name node_modules -print0)
|
||||
if [[ -d "$REPO_ROOT/node_modules" ]]; then DIRS_TO_DELETE+=("$REPO_ROOT/node_modules"); fi
|
||||
|
||||
section "Summary so far"
|
||||
echo "Found ${#DIRS_TO_DELETE[@]} node_modules directories"
|
||||
|
||||
if [[ "$INCLUDE_CACHE" == true ]]; then
|
||||
section "Scanning cache/output directories"
|
||||
while IFS= read -r -d '' d; do DIRS_TO_DELETE+=("$d"); done < <(find "$REPO_ROOT" -type d \( \
|
||||
-name .next -o -name .turbo -o -name .vite -o -name .parcel-cache -o -name .cache -o \
|
||||
-name dist -o -name build -o -name coverage -o -name out -o -name storybook-static -o -name .wrangler \
|
||||
\) -print0)
|
||||
fi
|
||||
|
||||
if [[ "$INCLUDE_LOCK" == true ]]; then
|
||||
section "Scanning lock files"
|
||||
while IFS= read -r -d '' f; do FILES_TO_DELETE+=("$f"); done < <(\
|
||||
find "$REPO_ROOT" -type f \( -name pnpm-lock.yaml -o -name package-lock.json -o -name yarn.lock \) -print0)
|
||||
fi
|
||||
|
||||
section "Summary"
|
||||
echo "Directories to delete: ${#DIRS_TO_DELETE[@]}"
|
||||
echo "Files to delete: ${#FILES_TO_DELETE[@]}"
|
||||
|
||||
if [[ $(preview) -eq 1 ]]; then
|
||||
echo "Preview mode (no deletions). Re-run with --yes to confirm."
|
||||
for d in "${DIRS_TO_DELETE[@]}"; do echo "[dir] $d"; done
|
||||
for f in "${FILES_TO_DELETE[@]}"; do echo "[file] $f"; done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
section "Deleting directories"
|
||||
for d in "${DIRS_TO_DELETE[@]}"; do rm -rf -- "$d" || true; done
|
||||
|
||||
if [[ ${#FILES_TO_DELETE[@]} -gt 0 ]]; then
|
||||
section "Deleting files"
|
||||
for f in "${FILES_TO_DELETE[@]}"; do rm -f -- "$f" || true; done
|
||||
fi
|
||||
|
||||
if [[ "$PRUNE_STORE" == true ]]; then
|
||||
section "Pruning pnpm store"
|
||||
if command -v pnpm >/dev/null 2>&1; then pnpm store prune || true; else echo "pnpm not found; skipping"; fi
|
||||
fi
|
||||
|
||||
echo "Done."
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Cleanup script for old GitHub Container Registry (GHCR) images
|
||||
# This script uses the 'gh' CLI to delete old package versions.
|
||||
# Requires 'gh' CLI to be installed and authenticated with 'delete:packages' scope.
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
ORG="asepharyana"
|
||||
PACKAGE_NAMES=("rust-api" "elysia-api" "nextjs-web")
|
||||
|
||||
echo "🚀 Starting GHCR cleanup for $ORG..."
|
||||
|
||||
for PACKAGE in "${PACKAGE_NAMES[@]}"; do
|
||||
echo "------------------------------------------------"
|
||||
echo "📦 Checking package: $PACKAGE"
|
||||
|
||||
# List versions that are NOT 'latest' and DON'T match the current SHAs
|
||||
# This is a safe approach: list all versions and let the user decide or
|
||||
# filter by date/tag patterns.
|
||||
|
||||
# For simplicity and safety, this script will list versions and
|
||||
# provide the command to delete them.
|
||||
# To AUTOMATICALLY delete, uncomment the 'gh api' call below.
|
||||
|
||||
echo "🔍 Fetching versions..."
|
||||
VERSIONS=$(gh api "/orgs/$ORG/packages/container/$PACKAGE/versions" --paginate -q '.[] | "\(.id) \(.metadata.container.tags[0] // "no-tag") \(.updated_at)"')
|
||||
|
||||
if [ -z "$VERSIONS" ]; then
|
||||
echo "✅ No versions found for $PACKAGE"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "$VERSIONS" | while read -r ID TAG DATE; do
|
||||
if [[ "$TAG" == "latest" ]]; then
|
||||
echo "✨ Skipping latest: $ID ($DATE)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Example: only delete if the tag doesn't start with 'sha-' (adjust as needed)
|
||||
# Or delete very old ones.
|
||||
|
||||
echo "🗑️ Found old version: $ID | Tag: $TAG | Date: $DATE"
|
||||
|
||||
# UNCOMMENT THE LINE BELOW TO ENABLE AUTOMATIC DELETION
|
||||
# gh api -X DELETE "/orgs/$ORG/packages/container/$PACKAGE/versions/$ID"
|
||||
# echo "✅ Deleted $ID"
|
||||
done
|
||||
done
|
||||
|
||||
echo "------------------------------------------------"
|
||||
echo "✅ Cleanup script finished."
|
||||
echo "💡 Note: Deletion is commented out by default for safety. Edit the script to enable it."
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "=== Setting up Git hooks ==="
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
|
||||
cat <<EOF
|
||||
ℹ️ This project uses Git submodules but does not enforce hooks via Husky anymore.
|
||||
|
||||
Each app submodule manages its own hooks independently.
|
||||
|
||||
To set up hooks locally, run:
|
||||
cp -r scripts/git-hooks/ .git/hooks/
|
||||
EOF
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "=== Syncing all submodules ==="
|
||||
|
||||
git submodule update --init --recursive
|
||||
|
||||
echo ""
|
||||
echo "=== Latest submodule status ==="
|
||||
git submodule status
|
||||
|
||||
echo ""
|
||||
echo "✅ All submodules synced"
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Define an array of directories to aggressively prune from the search tree
|
||||
# This prevents exhaustive traversal into massive localized dependency stores, build caches,
|
||||
# and anomalously created cache directories (e.g., literal '~').
|
||||
PRUNE_DIRS=(
|
||||
"node_modules"
|
||||
"dist"
|
||||
".next"
|
||||
".bun"
|
||||
"~"
|
||||
"build"
|
||||
".git"
|
||||
"target"
|
||||
)
|
||||
|
||||
# Construct the prune expression dynamically to feed the find command
|
||||
PRUNE_ARGS=()
|
||||
for dir in "${PRUNE_DIRS[@]}"; do
|
||||
if [ ${#PRUNE_ARGS[@]} -gt 0 ]; then
|
||||
PRUNE_ARGS+=("-o")
|
||||
fi
|
||||
PRUNE_ARGS+=("-name" "$dir")
|
||||
done
|
||||
|
||||
echo "Scanning for top-level and workspace package.json manifests..."
|
||||
|
||||
# Perform an optimized find:
|
||||
# 1. -prune halts traversal immediately upon matching a PRUNE_DIR, achieving extreme I/O efficiency.
|
||||
# 2. -print0 strictly streams zero-byte delimited paths, immunizing the loop against spaces/newlines.
|
||||
find . \( "${PRUNE_ARGS[@]}" \) -prune -o -name "package.json" -type f -print0 | while IFS= read -r -d '' filename; do
|
||||
dir=$(dirname "$filename")
|
||||
|
||||
echo "--------------------------------------------------------------------------------"
|
||||
echo "Initiating strict dependency update sequence in: $dir"
|
||||
|
||||
# Spawn a tightly scoped subshell. This isolates environment state and traps internal pathing failures.
|
||||
(
|
||||
# Strict directory entry checks.
|
||||
cd "$dir" || {
|
||||
echo "CRITICAL: Directory transition failed for $dir. Process aborted." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Heuristic check: Ensure the manifest actually declares dependencies before thrashing the disk with ncu.
|
||||
if ! grep -Eq '"(dependencies|devDependencies|peerDependencies)"[[:space:]]*:' package.json; then
|
||||
echo "Notice: No valid dependency blocks detected in $dir/package.json. Bypassing node traversal."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Delegate command resolution to bunx. This obliterates the 'command not found' failure mode
|
||||
# by dynamically sourcing the npm-check-updates binary, completely ignoring global namespace pollution.
|
||||
echo "Executing constraint-free updates via bunx..."
|
||||
bunx --bun npm-check-updates -u
|
||||
|
||||
echo "Commencing rigorous package installation phase..."
|
||||
bun install
|
||||
) || {
|
||||
echo "WARNING: Subshell failure encountered in $dir. Subsystem continues." >&2
|
||||
}
|
||||
done
|
||||
|
||||
echo "--------------------------------------------------------------------------------"
|
||||
echo "SYSTEM STATE: All traversable dependencies aggressively updated."
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
PROMETHEUS_VERSION="2.53.0"
|
||||
TAILSCALE_IP="100.108.1.124"
|
||||
PROMETHEUS_DIR="/etc/prometheus"
|
||||
PROMETHEUS_DATA_DIR="/var/lib/prometheus"
|
||||
|
||||
echo "Creating prometheus user and group..."
|
||||
if ! getent group prometheus >/dev/null; then
|
||||
groupadd --system prometheus
|
||||
fi
|
||||
|
||||
if ! getent passwd prometheus >/dev/null; then
|
||||
useradd --system -g prometheus --no-create-home --shell /usr/sbin/nologin prometheus
|
||||
fi
|
||||
|
||||
echo "Creating directories..."
|
||||
mkdir -p "$PROMETHEUS_DIR" "$PROMETHEUS_DATA_DIR"
|
||||
chown prometheus:prometheus "$PROMETHEUS_DIR" "$PROMETHEUS_DATA_DIR"
|
||||
|
||||
echo "Downloading Prometheus v${PROMETHEUS_VERSION}..."
|
||||
cd /tmp
|
||||
wget -qO prometheus.tar.gz "https://github.com/prometheus/prometheus/releases/download/v${PROMETHEUS_VERSION}/prometheus-${PROMETHEUS_VERSION}.linux-amd64.tar.gz"
|
||||
|
||||
echo "Extracting Prometheus..."
|
||||
tar -xf prometheus.tar.gz
|
||||
cd "prometheus-${PROMETHEUS_VERSION}.linux-amd64"
|
||||
|
||||
echo "Installing binaries..."
|
||||
install -m 0755 -o prometheus -g prometheus prometheus promtool /usr/local/bin/
|
||||
|
||||
echo "Installing consoles and libraries..."
|
||||
cp -a consoles console_libraries "$PROMETHEUS_DIR/"
|
||||
chown -R prometheus:prometheus "$PROMETHEUS_DIR/consoles" "$PROMETHEUS_DIR/console_libraries"
|
||||
|
||||
echo "Creating configuration file..."
|
||||
cat <<EOF > "${PROMETHEUS_DIR}/prometheus.yml"
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: "prometheus"
|
||||
static_configs:
|
||||
- targets: ["${TAILSCALE_IP}:9090"]
|
||||
EOF
|
||||
chown prometheus:prometheus "${PROMETHEUS_DIR}/prometheus.yml"
|
||||
chmod 0644 "${PROMETHEUS_DIR}/prometheus.yml"
|
||||
|
||||
echo "Creating systemd unit file..."
|
||||
cat <<EOF > /etc/systemd/system/prometheus.service
|
||||
[Unit]
|
||||
Description=Prometheus Time Series Collection and Processing Server
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
User=prometheus
|
||||
Group=prometheus
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/prometheus \\
|
||||
--config.file=${PROMETHEUS_DIR}/prometheus.yml \\
|
||||
--storage.tsdb.path=${PROMETHEUS_DATA_DIR} \\
|
||||
--web.console.templates=${PROMETHEUS_DIR}/consoles \\
|
||||
--web.console.libraries=${PROMETHEUS_DIR}/console_libraries \\
|
||||
--web.listen-address=${TAILSCALE_IP}:9090 \\
|
||||
--web.external-url=http://${TAILSCALE_IP}:9090/
|
||||
ExecReload=/bin/kill -HUP \$MAINPID
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
LimitNOFILE=65536
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
chmod 0644 /etc/systemd/system/prometheus.service
|
||||
|
||||
echo "Reloading systemd, enabling and starting prometheus..."
|
||||
systemctl daemon-reload
|
||||
systemctl enable prometheus
|
||||
systemctl restart prometheus
|
||||
|
||||
echo "Checking Prometheus status..."
|
||||
systemctl status prometheus --no-pager || true
|
||||
|
||||
echo "Prometheus setup completed successfully!"
|
||||
Reference in New Issue
Block a user