# Manual Deployment

## Preconditions

Deploy only an identified commit that passed `composer quality` and `npm run build`. Confirm backups, free disk space, database access, any required queue supervision, scheduler, TLS, and a rollback owner. Keep the production `.env`, persistent storage, and `APP_KEY` outside replaceable releases.

The **Quality** workflow (`.github/workflows/quality.yml`) runs on pushes and pull requests. The separate **Build deployment artifact** workflow (`.github/workflows/release-artifact.yml`) is manual and builds the exact Git ref or full commit SHA supplied through its required `ref` input. Do not deploy GitHub's automatic source ZIP as though it were this verified build artifact.

The following examples assume `/var/www/threegoats`, a `deploy` account, a `current` symlink, `shared/.env`, and `shared/storage`. Adapt explicit paths and service names to the approved host.

## Approach A: atomic release from Git

Prepare the new release without affecting live traffic:

```bash
APP_ROOT=/var/www/threegoats
: "${REPOSITORY_URL:?Set REPOSITORY_URL to the approved Git URL}"
RELEASE_ID=$(date -u +%Y%m%d%H%M%S)
RELEASE="$APP_ROOT/releases/$RELEASE_ID"
git clone --depth 1 --branch main "$REPOSITORY_URL" "$RELEASE"
cd "$RELEASE"
ln -s "$APP_ROOT/shared/.env" .env
rm -rf storage && ln -s "$APP_ROOT/shared/storage" storage
composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
composer check-platform-reqs --no-dev
npm ci && npm run build
php artisan config:clear
```

Continue in the same shell so `RELEASE` remains the staged path. Record the old target, enter maintenance mode, migrate, switch atomically, and warm caches:

```bash
APP_ROOT=/var/www/threegoats
: "${RELEASE:?Stage and verify a release before switching}"
OLD_RELEASE=$(readlink -f "$APP_ROOT/current")
test -d "$OLD_RELEASE" && test -d "$RELEASE"
php "$APP_ROOT/current/artisan" down --retry=60
cd "$RELEASE"
php artisan migrate --force
php artisan optimize
ln -sfn "$RELEASE" "$APP_ROOT/current"
php artisan storage:link
php artisan queue:restart
php artisan up
```

Smoke-test `/up`, `/`, authentication, `/admin`, assets, mail, and the selected queue driver. Retain several known-good releases, then prune only explicit old release directories under the approved retention policy.

## Approach B: atomic release from the workflow artifact

In GitHub, choose **Actions → Build deployment artifact → Run workflow**, enter the approved Git ref or full commit SHA in the required `ref` input, and select **Run workflow**. The equivalent CLI trigger is `gh workflow run release-artifact.yml -f ref=<git-ref-or-full-commit-SHA>`. Wait for that specific run to succeed.

The run uploads an artifact named `threegoats-club-<short-SHA>.zip`; it obtains the short SHA with `git rev-parse --short=7 HEAD`. Download it from that successful run's **Artifacts** section, or use the exact run ID and artifact name with `gh run download <run-id> --name threegoats-club-<short-SHA>.zip --dir <download-directory>`. The CLI download extracts the GitHub artifact wrapper and leaves the generated ZIP in the selected directory; a browser download may require extracting the wrapper first.

The generated `threegoats-club-<short-SHA>.zip` includes application source, optimized PHP 8.3-compatible production Composer dependencies, `public/build`, and root-level `BUILD_INFO.txt`. It excludes `.env`, tests, development dependencies, source-control metadata, `node_modules`, and local database files. Before transfer or deployment, inspect `BUILD_INFO.txt` and confirm `commit_sha` is the exact approved full commit and `target_php_version` is `8.3.6`. Record a SHA-256 checksum after download, then transfer the generated ZIP and checksum through an authenticated channel.

On the server, verify and stage the generated ZIP into a new release directory. Substitute the actual short SHA in `ARCHIVE`; continue in the same shell when running the switch commands from Approach A:

```bash
cd /var/www/threegoats/incoming
ARCHIVE=threegoats-club-<short-SHA>.zip
sha256sum -c "$ARCHIVE.sha256"
unzip -p "$ARCHIVE" BUILD_INFO.txt
APP_ROOT=/var/www/threegoats
RELEASE_ID=$(date -u +%Y%m%d%H%M%S)
RELEASE="$APP_ROOT/releases/$RELEASE_ID"
mkdir -p "$RELEASE"
unzip -q "$ARCHIVE" -d "$RELEASE"
cd "$RELEASE"
test -f BUILD_INFO.txt
test -f vendor/autoload.php
test -f public/build/manifest.json
ln -s "$APP_ROOT/shared/.env" .env
rm -rf storage && ln -s "$APP_ROOT/shared/storage" storage
php artisan config:clear
```

Use the same maintenance, migration, symlink switch, cache, worker restart, and smoke-test commands from Approach A. Remove incoming archives after acceptance according to policy; they may contain proprietary source even though they must not contain secrets.

## Workflow artifact identity

`BUILD_INFO.txt` records the artifact filename, repository, requested ref, full and short commit SHAs, UTC build time, workflow run identifiers, Laravel version, the build PHP version, target PHP version `8.3.6`, and Node.js 20. PHP 8.4 is not required. Keep it with the release and copy its full commit SHA and artifact name into the server acceptance record. Never guess an artifact name, reuse an artifact from another run, or deploy from a failed or in-progress run.

## Rollback

Application rollback switches `current` to the recorded prior release. Set `OLD_RELEASE` to the exact path recorded before deployment; the guard stops if it is unset:

```bash
APP_ROOT=/var/www/threegoats
: "${OLD_RELEASE:?Set OLD_RELEASE to the recorded prior release path}"
test -d "$OLD_RELEASE"
php "$APP_ROOT/current/artisan" down --retry=60
ln -sfn "$OLD_RELEASE" "$APP_ROOT/current"
cd "$APP_ROOT/current"
php artisan optimize:clear
php artisan optimize
php artisan queue:restart
php artisan up
```

Database rollback is separate and higher risk. Prefer backward-compatible expand/contract migrations so the prior application can run against the migrated schema. Run `php artisan migrate:rollback --step=1 --force` only when the exact migration was reviewed, its data impact is understood, and a verified backup exists. Restore persistent data only under an approved recovery plan.

## Failure handling and key caution

- If staging/build fails, do not enter maintenance mode or switch `current`.
- If migration/switch fails, capture non-sensitive errors, return to the old release when compatible, and run `up`.
- Never run `php artisan key:generate` in production or replace `APP_KEY` during a release/rollback. Loss or rotation without a migration plan can invalidate encrypted data and sessions.
- Never overwrite `.env` or persistent `storage` with release contents.
- Record release commit/checksum, timestamps, operator, commands, migration result, prior release, and acceptance result without secrets or PII.

No server validation, artifact build/download, or deployment was performed while writing this guide.