Explorar o código

Tooling: appctl prod upgrade — pin a prod box to a release tag

New `appctl prod upgrade [VER]` subcommand for redeploying a running
prod stack at a specific release. VER defaults to the `latest` git
tag (manually pointed at the current release; `git tag -f latest
vX.Y.Z` to bump). The keyword `test` is virtual and resolves to
`origin/main` HEAD after fetch, so bleeding-edge redeploys don't
need a real branch tag.

Flow on each invocation: dirty-tree check (warn + y/N prompt) →
`git fetch --tags origin` → resolve & verify the ref → compose down
→ `git checkout --detach <ref>` → compose build → compose up -d.
Refusing on an unknown ref happens before the stack is stopped.

Bash completion learns the new subcommand and, at the third word,
offers `latest`, `test`, and every `v*` tag from `git tag`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ClaudePriv@chiappa.zhdk.ch hai 14 horas
pai
achega
f87660e870
Modificáronse 2 ficheiros con 74 adicións e 8 borrados
  1. 63 7
      bin/appctl
  2. 11 1
      bin/appctl-completion.bash

+ 63 - 7
bin/appctl

@@ -41,9 +41,13 @@ DEV
   appctl dev logs        tail logs from the dev stack
 
 PROD
-  appctl prod start      start prod stack detached
-  appctl prod stop       stop and remove prod containers
-  appctl prod build      rebuild prod images
+  appctl prod start          start prod stack detached
+  appctl prod stop           stop and remove prod containers
+  appctl prod build          rebuild prod images
+  appctl prod upgrade [VER]  stop, fetch, checkout VER, rebuild, start
+                             VER defaults to the `latest` git tag;
+                             `test` is a virtual keyword that resolves
+                             to origin/main HEAD after fetch.
 
 CHECKS (one-shot containers, no running stack required)
   appctl lint            php -l on src/ + tests/
@@ -127,15 +131,67 @@ cmd_dev() {
 
 cmd_prod() {
     local sub="${1:-}"
+    shift || true
     case "$sub" in
-        start)  "${COMPOSE_PROD[@]}" up -d ;;
-        stop)   "${COMPOSE_PROD[@]}" down ;;
-        build)  "${COMPOSE_PROD[@]}" build ;;
+        start)    "${COMPOSE_PROD[@]}" up -d ;;
+        stop)     "${COMPOSE_PROD[@]}" down ;;
+        build)    "${COMPOSE_PROD[@]}" build ;;
+        upgrade)  cmd_prod_upgrade "$@" ;;
         ""|help|-h|--help) usage ;;
-        *) die "unknown prod subcommand: $sub (try: start|stop|build)" ;;
+        *) die "unknown prod subcommand: $sub (try: start|stop|build|upgrade)" ;;
     esac
 }
 
+# Pinned redeploy. Resolves VER (default `latest` tag; `test` ⇒ origin/main
+# HEAD after fetch) to a commit, then runs: stop → fetch → checkout →
+# build → start. Dirty trees prompt before any destructive step.
+cmd_prod_upgrade() {
+    local version="${1:-latest}"
+
+    if [[ -n "$(git status --porcelain)" ]]; then
+        printf 'appctl: working tree has uncommitted changes:\n' >&2
+        git status --short >&2
+        printf '\nThe checkout step will fail or clobber these. Continue anyway? [y/N] ' >&2
+        local reply
+        read -r reply || reply=""
+        case "$reply" in
+            y|Y|yes|YES) ;;
+            *) die "aborted by user (working tree dirty)" ;;
+        esac
+    fi
+
+    printf 'appctl: git fetch --tags origin...\n'
+    git fetch --tags origin
+
+    local target_ref
+    if [[ "$version" == "test" ]]; then
+        target_ref="origin/main"
+    else
+        target_ref="$version"
+    fi
+
+    if ! git rev-parse --verify --quiet "${target_ref}^{commit}" >/dev/null; then
+        die "unknown version: '$version' (expected a release tag, 'latest', or 'test')"
+    fi
+    local target_sha
+    target_sha="$(git rev-parse --short "${target_ref}^{commit}")"
+    printf 'appctl: upgrading to %s (%s)\n' "$version" "$target_sha"
+
+    printf 'appctl: stopping prod stack...\n'
+    "${COMPOSE_PROD[@]}" down
+
+    printf 'appctl: git checkout --detach %s...\n' "$target_ref"
+    git checkout --detach "$target_ref"
+
+    printf 'appctl: building prod images...\n'
+    "${COMPOSE_PROD[@]}" build
+
+    printf 'appctl: starting prod stack...\n'
+    "${COMPOSE_PROD[@]}" up -d
+
+    printf 'appctl: upgrade to %s (%s) complete.\n' "$version" "$target_sha"
+}
+
 cmd_lint() {
     "${COMPOSE_DEV[@]}" --profile test run --rm tests \
         sh -c 'find src tests -name "*.php" -print0 | xargs -0 -n1 -P 4 php -l > /dev/null && echo "lint: OK"'

+ 11 - 1
bin/appctl-completion.bash

@@ -26,12 +26,22 @@ _appctl_complete() {
                 ;;
             prod)
                 # shellcheck disable=SC2207
-                COMPREPLY=( $(compgen -W "start stop build" -- "${cur}") )
+                COMPREPLY=( $(compgen -W "start stop build upgrade" -- "${cur}") )
                 return 0
                 ;;
         esac
     fi
 
+    if (( cword == 3 )) \
+        && [[ "${COMP_WORDS[1]}" == "prod" ]] \
+        && [[ "${COMP_WORDS[2]}" == "upgrade" ]]; then
+        local versions
+        versions="latest test $(git tag --list 'v*' 2>/dev/null)"
+        # shellcheck disable=SC2207
+        COMPREPLY=( $(compgen -W "${versions}" -- "${cur}") )
+        return 0
+    fi
+
     return 0
 }