From 92016256ce44be011c423ca8983d6efd79e372c7 Mon Sep 17 00:00:00 2001 From: Konstantinos Arvanitakis Date: Wed, 30 Sep 2026 12:17:52 +0300 Subject: [PATCH] Feat: Adding core-mode and deploy scripts --- bin/core-mode | 146 ++++++++++++++++++++++++++++++++++++++++++++++++++ bin/deploy | 120 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100755 bin/core-mode create mode 100755 bin/deploy diff --git a/bin/core-mode b/bin/core-mode new file mode 100755 index 0000000..4125c8c --- /dev/null +++ b/bin/core-mode @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# Switch how this app consumes boboko-core, then restart the dev stack so the +# entrypoints re-resolve it. +# +# bin/core-mode local ../boboko-core checkout (Composer path repo + npm file:) +# bin/core-mode repo tagged releases from the forge (Composer VCS repo + npm git+semver) +# bin/core-mode print the current mode +# +# Running it for the mode you're already in skips the file edits but still +# restarts the stack — that's how repo mode picks up a newly pushed tag. +# +# The toggle lives in composer.json ("repositories" <-> "_repositories") and +# package.json (@boboko/core in "dependencies" <-> "_dependencies"). The dev +# entrypoints run `composer update boboko/*` and `npm update @boboko/core` on +# every boot, which rewrites composer.lock / package-lock.json for the mode. +set -euo pipefail + +cd "$(dirname "$0")/.." + +fail() { echo "!!! $*" >&2; exit 1; } + +# Escape a string for use as a sed regex / replacement with "|" as delimiter. +sed_pattern() { printf '%s' "$1" | sed 's/[][\.*^$|]/\\&/g'; } +sed_replacement() { printf '%s' "$1" | sed 's/[\&|]/\\&/g'; } + +current_mode() { + local composer_type npm_dep composer_mode npm_mode + # The live block is "repositories" — the leading quote keeps "_repositories" out. + composer_type=$(sed -n '/"repositories": \[/,/"type"/p' composer.json | grep -o '"type": "[a-z]*"' | head -1) + npm_dep=$(sed -n '/"dependencies": {/,/}/p' package.json | grep -o '"@boboko/core": "[^"]*"' | head -1) + + case "$composer_type" in + *'"path"') composer_mode=local ;; + *'"vcs"') composer_mode=repo ;; + *) fail "Can't tell the mode from composer.json's \"repositories\" block." ;; + esac + case "$npm_dep" in + *'"file:'*) npm_mode=local ;; + *'"git+'*) npm_mode=repo ;; + *) fail "Can't tell the mode from package.json's @boboko/core dependency." ;; + esac + [ "$composer_mode" = "$npm_mode" ] || + fail "composer.json ($composer_mode) and package.json ($npm_mode) disagree — fix by hand first." + + echo "$composer_mode" +} + +swap_files() { + # composer.json: swap the key names, values stay where they are. + sed -i \ + -e 's/"_repositories":/"__core_mode_swap__":/' \ + -e 's/"repositories":/"_repositories":/' \ + -e 's/"__core_mode_swap__":/"repositories":/' \ + composer.json + + # package.json: swap the two @boboko/core values (live and parked). + local a b + a=$(grep -o '"@boboko/core": "[^"]*"' package.json | sed -n 1p) + b=$(grep -o '"@boboko/core": "[^"]*"' package.json | sed -n 2p) + [ -n "$a" ] && [ -n "$b" ] || fail "Expected two @boboko/core entries in package.json." + sed -i \ + -e "s|$(sed_pattern "$a")|__core_mode_swap__|" \ + -e "s|$(sed_pattern "$b")|$(sed_replacement "$a")|" \ + -e "s|__core_mode_swap__|$(sed_replacement "$b")|" \ + package.json +} + +wrapper_for() { + if [ "$1" = local ]; then echo bin/dc-core.sh; else echo bin/dc; fi +} + +# The app healthcheck (php-fpm -t) passes before the entrypoint finishes, so +# wait on the entrypoints' own log lines instead: the app's composer block is +# done once it clears caches, the vite container's npm block once Vite is up. +wait_for_log() { + local dc=$1 service=$2 pattern=$3 label=$4 waited=0 + printf '>>> Waiting for %s' "$label" + until $dc logs --no-color "$service" 2>&1 | grep -q "$pattern"; do + if [ -z "$($dc ps -q --status running "$service")" ]; then + echo + $dc logs --no-color --tail 40 "$service" + fail "$service stopped before finishing (logs above)." + fi + if [ "$waited" -ge 600 ]; then + echo + $dc logs --no-color --tail 40 "$service" + fail "Timed out after 600s waiting for $service (logs above)." + fi + printf '.' + sleep 3 + waited=$((waited + 3)) + done + echo " done" +} + +mode=$(current_mode) + +if [ $# -eq 0 ]; then + echo "$mode" + exit 0 +fi + +target=$1 +[ "$target" = local ] || [ "$target" = repo ] || fail "Usage: bin/core-mode [local|repo]" +if [ "$target" = local ] && [ ! -d ../boboko-core ]; then + fail "../boboko-core not found — local mode needs the core checkout next to this repo." +fi + +old_dc=$(wrapper_for "$mode") +new_dc=$(wrapper_for "$target") + +echo ">>> Stopping stack ($mode mode)..." +$old_dc down + +if [ "$mode" != "$target" ]; then + echo ">>> Switching composer.json / package.json: $mode -> $target" + swap_files + [ "$(current_mode)" = "$target" ] || fail "Switch didn't take — check composer.json / package.json." +else + echo ">>> Already in $target mode, restarting to re-resolve boboko-core" +fi + +# The entrypoints run `composer install` / `npm install` from the lockfiles +# BEFORE re-resolving core. Local-mode lockfiles point at ../boboko-core, +# which isn't mounted in repo mode, so the install fails. The committed +# lockfiles are always repo mode — restore those and let the entrypoints bump +# them to the newest tag. +if [ "$target" = repo ]; then + if grep -A14 '"name": "boboko/core"' composer.lock | grep -q '"type": "path"' || + grep -A6 '"node_modules/@boboko/core": {' package-lock.json | grep -q '"link": true'; then + echo ">>> Lockfiles still point at ../boboko-core — restoring the committed ones" + git checkout HEAD -- composer.lock package-lock.json + fi +fi + +echo ">>> Starting stack ($target mode)..." +if ! $new_dc up -d; then + $new_dc logs --no-color --tail 40 app + fail "Stack failed to start (app logs above)." +fi + +wait_for_log "$new_dc" app '\[entrypoint\] Caches cleared' "composer to re-resolve boboko/core" +wait_for_log "$new_dc" vite 'ready in' "npm to re-resolve @boboko/core" + +version=$(grep -A1 '"name": "boboko/core"' composer.lock | grep -o '"version": "[^"]*"' | cut -d'"' -f4) +echo ">>> Now in $target mode — boboko/core $version" diff --git a/bin/deploy b/bin/deploy new file mode 100755 index 0000000..bd718b2 --- /dev/null +++ b/bin/deploy @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# Deploy this app to production with the latest boboko-core tag. +# +# 1. Refuses to run with uncommitted work (the server deploys what's pushed, +# nothing else) — only the core wiring files are allowed to be dirty. +# 2. Pulls translation lines added in the Language Lines UI into core's +# seeders (boboko:translations:pull) — in local mode; if any are pulled, +# stops so you can release core first. In repo mode it only checks. +# 3. Switches to repo mode if you're on the local ../boboko-core checkout, +# and restarts the stack either way so the lockfiles pick up the newest +# tag (bin/core-mode repo). +# 4. Commits the lockfile bump, if any, asks for confirmation, pushes. +# 5. Runs `envoy run deploy` (see Envoy.blade.php) against .env.envoy's host. +# +# Leaves you in repo mode — run `bin/core-mode local` to go back. +set -euo pipefail + +cd "$(dirname "$0")/.." + +fail() { echo "!!! $*" >&2; exit 1; } + +[ -f .env.envoy ] || fail ".env.envoy not found — copy .env.envoy.example and fill it in." +branch=$(grep -m1 '^DEPLOY_BRANCH=' .env.envoy | cut -d= -f2-) +host=$(grep -m1 '^DEPLOY_SSH_HOST=' .env.envoy | cut -d= -f2-) +branch=${branch:-master} + +[ "$(git branch --show-current)" = "$branch" ] || fail "You're not on $branch (the branch Envoy deploys)." + +# Anything tracked and modified other than the four core wiring files would be +# silently left out of the deploy — make that a hard stop instead. +dirty=$(git status --porcelain --untracked-files=no | + grep -vE '^.. (composer\.json|composer\.lock|package\.json|package-lock\.json)$' || true) +if [ -n "$dirty" ]; then + echo "$dirty" + fail "Commit or stash these first — the server only deploys what's pushed." +fi + +echo ">>> Fetching origin..." +git fetch origin "$branch" +[ "$(git rev-list --count "HEAD..origin/$branch")" -eq 0 ] || fail "origin/$branch has commits you don't — pull first." + +# Translation lines added through the Language Lines UI only reach other apps +# once they're in core's seeders. In local mode, pull them into +# ../boboko-core; in repo mode the command can only report them. +lines_in() { printf '%s\n' "$1" | sed -n 's/^\([0-9][0-9]*\) line(s).*/\1/p' | tail -1; } + +if [ "$(bin/core-mode)" = local ]; then + echo ">>> Pulling UI-added translations into ../boboko-core's seeders..." + out=$(bin/dc-core.sh exec -T app php artisan boboko:translations:pull --no-ansi) || + fail "boboko:translations:pull failed — is the stack running (bin/dc-core.sh up -d)?" + echo "$out" + pulled=$(lines_in "$out") + if [ "${pulled:-0}" -gt 0 ]; then + fail "Pulled $pulled translation line(s) into ../boboko-core. Review, commit, tag and push core, then rerun bin/deploy." + fi +else + echo ">>> Checking for UI-added translations missing from core's seeders..." + if out=$(bin/dc exec -T app php artisan boboko:translations:pull --dry-run --no-ansi 2>&1); then + missing=$(lines_in "$out") + if [ "${missing:-0}" -gt 0 ]; then + echo "$out" + fail "These lines are only in this database — run bin/core-mode local and rerun bin/deploy to pull them into core." + fi + echo " nothing missing" + else + echo " skipped — the installed boboko/core doesn't have boboko:translations:pull yet" + fi +fi + +# Switch to repo mode (or just restart if already there) so both lockfiles +# resolve the newest boboko-core tag. +bin/core-mode repo + +git diff --quiet HEAD -- composer.json package.json || + fail "composer.json / package.json differ from HEAD in repo mode — they should only differ by the local/repo toggle." +if grep -A14 '"name": "boboko/core"' composer.lock | grep -q '"type": "path"'; then + fail "composer.lock still points boboko/core at the local path repo." +fi +if grep -A6 '"node_modules/@boboko/core": {' package-lock.json | grep -q '"link": true'; then + fail "package-lock.json still links @boboko/core to the local checkout." +fi + +version=$(grep -A1 '"name": "boboko/core"' composer.lock | grep -o '"version": "[^"]*"' | cut -d'"' -f4) + +# A tag you created in ../boboko-core but never pushed won't be resolved. +if [ -d ../boboko-core ]; then + latest=$(git -C ../boboko-core tag --sort=-v:refname | head -1) + if [ -n "$latest" ] && [ "$latest" != "v$version" ]; then + echo "!!! Warning: resolved boboko/core $version, but ../boboko-core's newest tag is $latest." + echo "!!! Did you push it? (git -C ../boboko-core push origin $latest)" + fi +fi + +if ! git diff --quiet HEAD -- composer.lock package-lock.json; then + echo ">>> Committing lockfiles for boboko/core $version..." + git add composer.lock package-lock.json + git commit -m "Chore: Bumping boboko/core to $version" +fi + +echo +echo ">>> Deploying $branch to $host with boboko/core $version." +pending=$(git log --oneline "origin/$branch..HEAD") +if [ -n "$pending" ]; then + echo ">>> Commits to push:" + echo "$pending" +else + echo ">>> Nothing new to push — this redeploys origin/$branch as-is." +fi +echo +read -r -p "Push and deploy? [y/N] " answer +[ "$answer" = y ] || [ "$answer" = Y ] || fail "Aborted." + +if [ -n "$pending" ]; then + git push origin "$branch" +fi + +vendor/bin/envoy run deploy + +echo +echo ">>> Done. You're in repo mode — run bin/core-mode local to go back to your ../boboko-core checkout."