fix(website): depend on @arkyc/{widget,types} via workspace, not npm … #61
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |