Skip to content

fix(website): depend on @arkyc/{widget,types} via workspace, not npm … #61

fix(website): depend on @arkyc/{widget,types} via workspace, not npm …

fix(website): depend on @arkyc/{widget,types} via workspace, not npm … #61

Workflow file for this run

name: Deploy API, Dashboard & Website
on:
push:
branches:
- staging
- production
workflow_dispatch:
permissions:
contents: read
# --- Production topology (three TinyCP sites, three docroots) -----------------
# api.arkyc.toneflix.net -> /var/www/api.arkyc.toneflix.net (monorepo build
# checkout + the running API; its root .nginx proxies
# to the Express process on :3100)
# app.arkyc.toneflix.net -> /var/www/app.arkyc.toneflix.net (dashboard static)
# arkyc.toneflix.net -> /var/www/arkyc.toneflix.net (website static;
# + www alias www.arkyc.toneflix.net is a TinyCP alias of the apex)
#
# Everything builds in the one monorepo checkout (the API docroot); the two SPA
# bundles are published into their own docroots, each with its own .nginx include.
# Paths/domains are hardcoded on purpose — they aren't secrets and staying explicit
# keeps the open-source deploy auditable. Only SSH creds + Firebase live in secrets.
jobs:
deploy-staging:
name: Staging Deployment
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/staging'
runs-on: ubuntu-latest
environment: staging
concurrency:
group: deploy-staging
cancel-in-progress: true
steps:
- name: Deploy all apps on staging server
uses: appleboy/ssh-action@v1
id: ssh
with:
key: ${{ secrets.SSH_PRIVATE_KEY }}
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
port: ${{ secrets.SSH_PORT }}
capture_stdout: true
script: |
set -e
PNPM_VERSION="10.6.1"
DEPLOY_BRANCH="${{ github.ref_name }}"
# --- TinyCP sites / docroots (see header) ---
API_DOCROOT="/var/www/api.arkyc.toneflix.net"
APP_DOCROOT="/var/www/app.arkyc.toneflix.net"
WEB_DOCROOT="/var/www/arkyc.toneflix.net"
API_ORIGIN="https://api.arkyc.toneflix.net"
DEPLOY_PATH="$API_DOCROOT" # build checkout + API site docroot
SHARED="$DEPLOY_PATH/shared" # persistent across deploys
RELEASE="$DEPLOY_PATH/current" # the standalone API artifact we run
# --- Update the build checkout (build source + API docroot) ---
cd "$DEPLOY_PATH"
git config --global --add safe.directory "$DEPLOY_PATH"
git fetch --all --prune
git checkout "$DEPLOY_BRANCH"
git reset --hard "origin/$DEPLOY_BRANCH"
source ~/.nvm/nvm.sh
echo "node $(node -v) · pnpm $(pnpm -v) · pm2 $(pm2 -v)"
corepack enable
corepack prepare "pnpm@${PNPM_VERSION}" --activate
# --- Persistent shared state ($DEPLOY_PATH/shared): .env + storage ---
mkdir -p "$SHARED/storage/app"
# Keep .env in the shared dir. On first run, adopt an existing real
# apps/api/.env (config from a prior setup); otherwise seed from the
# example and stop, so placeholder DB creds are never used.
if [ ! -f "$SHARED/.env" ]; then
if [ -f apps/api/.env ] && [ ! -L apps/api/.env ]; then
cp apps/api/.env "$SHARED/.env"
echo "Adopted existing apps/api/.env into $SHARED/.env"
else
cp apps/api/.env.example "$SHARED/.env"
echo "::error::Seeded $SHARED/.env from .env.example — set APP_KEY, DATABASE_URL, etc. then re-run."
exit 1
fi
fi
if [ -n "${{ secrets.FIREBASE_ADMINSDK }}" ]; then
printf '%s\n' '${{ secrets.FIREBASE_ADMINSDK }}' > "$SHARED/storage/app/firebase-adminsdk.json"
chmod 600 "$SHARED/storage/app/firebase-adminsdk.json"
fi
# The build checkout reads the same .env for migrations.
ln -sfn "$SHARED/.env" apps/api/.env
# --- Build everything this server serves ---
pnpm install --frozen-lockfile
pnpm run build:libs
pnpm --filter @arkyc/api build
# The dashboard bakes the API origin into its static bundle at build
# time (apps/dashboard/.env is gitignored, so set it here explicitly).
VITE_API_ENV=production \
VITE_APP_ENV=production \
VITE_API_URL="$API_ORIGIN" \
VITE_PROD_API_URL="$API_ORIGIN" \
pnpm --filter @arkyc/dashboard build
# The website bakes the dashboard + docs URLs into its static bundle
# (apps/website/.env is gitignored, so set them here explicitly).
VITE_API_ENV=production \
VITE_API_URL="$API_ORIGIN" \
VITE_PROD_API_URL="$API_ORIGIN" \
VITE_DASHBOARD_URL="https://app.arkyc.toneflix.net" \
VITE_DOCS_URL="https://docs.arkyc.toneflix.net" \
pnpm --filter @arkyc/website build
# --- App key: generated once on first setup, only when APP_KEY is
# unset. Regenerating would break encrypted data (2FA) + sessions. ---
if ! grep -qE '^APP_KEY=.+' "$SHARED/.env"; then
( cd apps/api && NODE_ENV=production pnpm ark key:generate --no-interaction )
# Ensure the generated key persists to the shared .env; keep the symlink.
cp -L apps/api/.env "$SHARED/.env" 2>/dev/null || true
ln -sfn "$SHARED/.env" apps/api/.env
fi
# --- Migrations + route count run from apps/api, where the `ark` CLI
# resolves (it isn't on PATH at the workspace root). NODE_ENV=production
# makes ark load resources from `dist` (matching the running server),
# not from `src` — so the route count reflects the deployed artifact. ---
( cd apps/api && NODE_ENV=production pnpm ark migrate )
ROUTE_LIST_OUTPUT="$(cd apps/api && NODE_ENV=production pnpm ark route:list)"
printf '%s\n' "$ROUTE_LIST_OUTPUT"
ROUTES_COUNT="$(printf '%s\n' "$ROUTE_LIST_OUTPUT" | sed -nE 's/.*Total routes[[:space:]]+([0-9]+).*/\1/p' | tail -n 1)"
if [ -z "$ROUTES_COUNT" ]; then
echo "Unable to extract the published route count."
exit 1
fi
# --- Build the standalone API artifact (flat node_modules, prod-only) ---
rm -rf "$RELEASE"
# --legacy: pnpm v10's default deploy requires inject-workspace-packages;
# legacy mode resolves the @arkyc/* workspace deps without that global flag.
pnpm --filter=@arkyc/api deploy --prod --legacy "$RELEASE"
# `dist` (compiled server) and `.arkstack` (framework manifest + the
# tsconfig the route loader reads at runtime) are gitignored, so pnpm
# deploy skips them — copy both into the artifact.
rm -rf "$RELEASE/dist" "$RELEASE/.arkstack"
cp -r apps/api/dist "$RELEASE/dist"
cp -r apps/api/.arkstack "$RELEASE/.arkstack"
# arkormx writes the camelCase -> column map to .arkormx/column-mappings.json
# at migrate time; it's gitignored, so pnpm deploy skips it. The server reads
# it relative to its cwd (current/), so ship the freshly-generated map into the
# artifact — without it the ORM falls back to raw attribute names and queries
# blow up with `column "firstName" does not exist`.
if [ -d apps/api/.arkormx ]; then
cp -r apps/api/.arkormx "$RELEASE/.arkormx"
else
echo "::warning::apps/api/.arkormx missing after migrate — column mappings will be absent in the artifact."
fi
# Wire shared state into the artifact.
ln -sfn "$SHARED/.env" "$RELEASE/.env"
ln -sfn "$SHARED/storage" "$RELEASE/storage"
# Public storage symlinks (the `ark storage:link` equivalent — ark isn't
# bundled in the prod artifact). Targets resolve via current/storage -> shared.
mkdir -p "$RELEASE/public" "$SHARED/storage/app/public" "$SHARED/storage/app/organizations"
ln -sfn "$RELEASE/storage/app/public" "$RELEASE/public/storage"
ln -sfn "$RELEASE/storage/app/organizations" "$RELEASE/public/organizations"
# --- (Re)start the standalone API under PM2 ---
pm2 startOrReload "$DEPLOY_PATH/ecosystem.config.cjs" --update-env
pm2 save
# --- Publish the static SPAs into their own site docroots ---
# Each docroot is a separate TinyCP site that includes its .nginx (SPA
# fallback). Refresh the hashed assets dir and overwrite index.html etc;
# leave the rest of the docroot (e.g. .well-known for Let's Encrypt) alone.
mkdir -p "$APP_DOCROOT"
rm -rf "$APP_DOCROOT/assets"
cp -rT apps/dashboard/dist "$APP_DOCROOT"
cp apps/dashboard/.nginx "$APP_DOCROOT/.nginx"
mkdir -p "$WEB_DOCROOT"
rm -rf "$WEB_DOCROOT/assets"
cp -rT apps/website/dist "$WEB_DOCROOT"
cp apps/website/.nginx "$WEB_DOCROOT/.nginx"
# Reload nginx so every site picks up its .nginx include (API proxy + SPAs).
# Guarded: no-ops on non-nginx hosts or when the caller can't reload it.
if command -v nginx >/dev/null 2>&1; then
nginx -t && { systemctl reload nginx 2>/dev/null || nginx -s reload; } || echo "nginx reload skipped"
fi
echo "$API_ORIGIN|$DEPLOY_BRANCH|$ROUTES_COUNT"
- name: Extract deploy meta
id: deploy_meta
run: |
cat <<'DEPLOY_EOF' > /tmp/deploy_output.txt
${{ steps.ssh.outputs.stdout }}
DEPLOY_EOF
raw=$(grep -oP 'https://[^|]+\|.+' /tmp/deploy_output.txt | tail -n 1)
url="${raw%%|*}"
deploy_meta="${raw#*|}"
branch="${deploy_meta%%|*}"
routes_count="${raw##*|}"
echo "url=$url" >> $GITHUB_OUTPUT
echo "branch=$branch" >> $GITHUB_OUTPUT
echo "routes_count=$routes_count" >> $GITHUB_OUTPUT
- name: Publish summary
if: '${{steps.deploy_meta.outputs.url && steps.deploy_meta.outputs.branch && steps.deploy_meta.outputs.routes_count}}'
run: |
URL="${{steps.deploy_meta.outputs.url}}"
BRANCH="${{steps.deploy_meta.outputs.branch}}"
ROUTES_COUNT="${{steps.deploy_meta.outputs.routes_count}}"
echo "✅ Successfully Deployed Staging." >> $GITHUB_STEP_SUMMARY
echo "Deploy Branch: $BRANCH" >> $GITHUB_STEP_SUMMARY
echo "Website: https://arkyc.toneflix.net" >> $GITHUB_STEP_SUMMARY
echo "Dashboard: https://app.arkyc.toneflix.net" >> $GITHUB_STEP_SUMMARY
echo "API: $URL" >> $GITHUB_STEP_SUMMARY
echo "Published Routes: $ROUTES_COUNT" >> $GITHUB_STEP_SUMMARY
echo "Health Check: $URL/health?human" >> $GITHUB_STEP_SUMMARY
deploy-production:
name: Production Deployment
if: github.event_name == 'push' && github.ref == 'refs/heads/production'
runs-on: ubuntu-latest
environment: production
concurrency:
group: deploy-production
cancel-in-progress: true
steps:
- name: Deploy all apps on production server
uses: appleboy/ssh-action@v1
id: ssh
with:
key: ${{ secrets.SSH_PRIVATE_KEY }}
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
port: ${{ secrets.SSH_PORT }}
capture_stdout: true
script: |
set -e
PNPM_VERSION="10.6.1"
DEPLOY_BRANCH="production"
# --- TinyCP sites / docroots (see header) ---
API_DOCROOT="/var/www/api.arkyc.toneflix.net"
APP_DOCROOT="/var/www/app.arkyc.toneflix.net"
WEB_DOCROOT="/var/www/arkyc.toneflix.net"
API_ORIGIN="https://api.arkyc.toneflix.net"
DEPLOY_PATH="$API_DOCROOT" # build checkout + API site docroot
SHARED="$DEPLOY_PATH/shared" # persistent across deploys
RELEASE="$DEPLOY_PATH/current" # the standalone API artifact we run
# --- Update the build checkout (build source + API docroot) ---
cd "$DEPLOY_PATH"
git config --global --add safe.directory "$DEPLOY_PATH"
git fetch --all --prune
git checkout "$DEPLOY_BRANCH"
git reset --hard "origin/$DEPLOY_BRANCH"
source ~/.nvm/nvm.sh
echo "node $(node -v) · pnpm $(pnpm -v) · pm2 $(pm2 -v)"
corepack enable
corepack prepare "pnpm@${PNPM_VERSION}" --activate
# --- Persistent shared state ($DEPLOY_PATH/shared): .env + storage ---
mkdir -p "$SHARED/storage/app"
# Keep .env in the shared dir. On first run, adopt an existing real
# apps/api/.env (config from a prior setup); otherwise seed from the
# example and stop, so placeholder DB creds are never used.
if [ ! -f "$SHARED/.env" ]; then
if [ -f apps/api/.env ] && [ ! -L apps/api/.env ]; then
cp apps/api/.env "$SHARED/.env"
echo "Adopted existing apps/api/.env into $SHARED/.env"
else
cp apps/api/.env.example "$SHARED/.env"
echo "::error::Seeded $SHARED/.env from .env.example — set APP_KEY, DATABASE_URL, etc. then re-run."
exit 1
fi
fi
if [ -n "${{ secrets.FIREBASE_ADMINSDK }}" ]; then
printf '%s\n' '${{ secrets.FIREBASE_ADMINSDK }}' > "$SHARED/storage/app/firebase-adminsdk.json"
chmod 600 "$SHARED/storage/app/firebase-adminsdk.json"
fi
# The build checkout reads the same .env for migrations.
ln -sfn "$SHARED/.env" apps/api/.env
# --- Build everything this server serves ---
pnpm install --frozen-lockfile
pnpm run build:libs
pnpm --filter @arkyc/api build
# The dashboard bakes the API origin into its static bundle at build
# time (apps/dashboard/.env is gitignored, so set it here explicitly).
VITE_API_ENV=production \
VITE_APP_ENV=production \
VITE_API_URL="$API_ORIGIN" \
VITE_PROD_API_URL="$API_ORIGIN" \
pnpm --filter @arkyc/dashboard build
# The website bakes the dashboard + docs URLs into its static bundle
# (apps/website/.env is gitignored, so set them here explicitly).
VITE_API_ENV=production \
VITE_API_URL="$API_ORIGIN" \
VITE_PROD_API_URL="$API_ORIGIN" \
VITE_DASHBOARD_URL="https://app.arkyc.toneflix.net" \
VITE_DOCS_URL="https://docs.arkyc.toneflix.net" \
pnpm --filter @arkyc/website build
# --- App key: generated once on first setup, only when APP_KEY is
# unset. Regenerating would break encrypted data (2FA) + sessions. ---
if ! grep -qE '^APP_KEY=.+' "$SHARED/.env"; then
( cd apps/api && NODE_ENV=production pnpm ark key:generate --no-interaction )
# Ensure the generated key persists to the shared .env; keep the symlink.
cp -L apps/api/.env "$SHARED/.env" 2>/dev/null || true
ln -sfn "$SHARED/.env" apps/api/.env
fi
# --- Migrations + route count run from apps/api, where the `ark` CLI
# resolves (it isn't on PATH at the workspace root). NODE_ENV=production
# makes ark load resources from `dist` (matching the running server),
# not from `src` — so the route count reflects the deployed artifact. ---
( cd apps/api && NODE_ENV=production pnpm ark migrate )
ROUTE_LIST_OUTPUT="$(cd apps/api && NODE_ENV=production pnpm ark route:list)"
printf '%s\n' "$ROUTE_LIST_OUTPUT"
ROUTES_COUNT="$(printf '%s\n' "$ROUTE_LIST_OUTPUT" | sed -nE 's/.*Total routes[[:space:]]+([0-9]+).*/\1/p' | tail -n 1)"
if [ -z "$ROUTES_COUNT" ]; then
echo "Unable to extract the published route count."
exit 1
fi
# --- Build the standalone API artifact (flat node_modules, prod-only) ---
rm -rf "$RELEASE"
# --legacy: pnpm v10's default deploy requires inject-workspace-packages;
# legacy mode resolves the @arkyc/* workspace deps without that global flag.
pnpm --filter=@arkyc/api deploy --prod --legacy "$RELEASE"
# `dist` (compiled server) and `.arkstack` (framework manifest + the
# tsconfig the route loader reads at runtime) are gitignored, so pnpm
# deploy skips them — copy both into the artifact.
rm -rf "$RELEASE/dist" "$RELEASE/.arkstack"
cp -r apps/api/dist "$RELEASE/dist"
cp -r apps/api/.arkstack "$RELEASE/.arkstack"
# arkormx writes the camelCase -> column map to .arkormx/column-mappings.json
# at migrate time; it's gitignored, so pnpm deploy skips it. The server reads
# it relative to its cwd (current/), so ship the freshly-generated map into the
# artifact — without it the ORM falls back to raw attribute names and queries
# blow up with `column "firstName" does not exist`.
if [ -d apps/api/.arkormx ]; then
cp -r apps/api/.arkormx "$RELEASE/.arkormx"
else
echo "::warning::apps/api/.arkormx missing after migrate — column mappings will be absent in the artifact."
fi
# Wire shared state into the artifact.
ln -sfn "$SHARED/.env" "$RELEASE/.env"
ln -sfn "$SHARED/storage" "$RELEASE/storage"
# Public storage symlinks (the `ark storage:link` equivalent — ark isn't
# bundled in the prod artifact). Targets resolve via current/storage -> shared.
mkdir -p "$RELEASE/public" "$SHARED/storage/app/public" "$SHARED/storage/app/organizations"
ln -sfn "$RELEASE/storage/app/public" "$RELEASE/public/storage"
ln -sfn "$RELEASE/storage/app/organizations" "$RELEASE/public/organizations"
# --- (Re)start the standalone API under PM2 ---
pm2 startOrReload "$DEPLOY_PATH/ecosystem.config.cjs" --update-env
pm2 save
# --- Publish the static SPAs into their own site docroots ---
# Each docroot is a separate TinyCP site that includes its .nginx (SPA
# fallback). Refresh the hashed assets dir and overwrite index.html etc;
# leave the rest of the docroot (e.g. .well-known for Let's Encrypt) alone.
mkdir -p "$APP_DOCROOT"
rm -rf "$APP_DOCROOT/assets"
cp -rT apps/dashboard/dist "$APP_DOCROOT"
cp apps/dashboard/.nginx "$APP_DOCROOT/.nginx"
mkdir -p "$WEB_DOCROOT"
rm -rf "$WEB_DOCROOT/assets"
cp -rT apps/website/dist "$WEB_DOCROOT"
cp apps/website/.nginx "$WEB_DOCROOT/.nginx"
# Reload nginx so every site picks up its .nginx include (API proxy + SPAs).
# Guarded: no-ops on non-nginx hosts or when the caller can't reload it.
if command -v nginx >/dev/null 2>&1; then
nginx -t && { systemctl reload nginx 2>/dev/null || nginx -s reload; } || echo "nginx reload skipped"
fi
echo "$API_ORIGIN|$DEPLOY_BRANCH|$ROUTES_COUNT"
- name: Extract deploy meta
id: deploy_meta
run: |
cat <<'DEPLOY_EOF' > /tmp/deploy_output.txt
${{ steps.ssh.outputs.stdout }}
DEPLOY_EOF
raw=$(grep -oP 'https://[^|]+\|.+' /tmp/deploy_output.txt | tail -n 1)
url="${raw%%|*}"
deploy_meta="${raw#*|}"
branch="${deploy_meta%%|*}"
routes_count="${raw##*|}"
echo "url=$url" >> $GITHUB_OUTPUT
echo "branch=$branch" >> $GITHUB_OUTPUT
echo "routes_count=$routes_count" >> $GITHUB_OUTPUT
- name: Publish summary
if: '${{steps.deploy_meta.outputs.url && steps.deploy_meta.outputs.branch && steps.deploy_meta.outputs.routes_count}}'
run: |
URL="${{steps.deploy_meta.outputs.url}}"
BRANCH="${{steps.deploy_meta.outputs.branch}}"
ROUTES_COUNT="${{steps.deploy_meta.outputs.routes_count}}"
echo "✅ Successfully Deployed Production." >> $GITHUB_STEP_SUMMARY
echo "Deploy Branch: $BRANCH" >> $GITHUB_STEP_SUMMARY
echo "Website: https://arkyc.toneflix.net" >> $GITHUB_STEP_SUMMARY
echo "Dashboard: https://app.arkyc.toneflix.net" >> $GITHUB_STEP_SUMMARY
echo "API: $URL" >> $GITHUB_STEP_SUMMARY
echo "Published Routes: $ROUTES_COUNT" >> $GITHUB_STEP_SUMMARY
echo "Health Check: $URL/health?human" >> $GITHUB_STEP_SUMMARY