# Upgrade

Source: https://docs.externa.qiick.io/docs/upgrade

Stay on a newer Externa release after first install. **Code** comes from Git (or a Docker image tag). **`php artisan externa:upgrade`** runs migrate / permissions sync / cache clear by default — it does **not** pull code unless you pass **opt-in** `--sync-upstream`.

> **Related**
> [Packagist & create-project](/docs/packagist) · [Installation](/docs/installation) · [Quick Start](/docs/quick-start) · [Deploy with Docker](/docs/deploy-with-docker) · [Backup & restore](/docs/backup-restore) · [Deployment](/docs/deployment) · [Operations](/docs/operations) · [core README](https://github.com/qiick-io/externa-core#quick-start)

## Mental model (read this first)

| Fact                          | Meaning                                                                                                                                                                                              |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `composer create-project`     | **Install once.** Copies the app tree into a new directory. It is **not** how you receive later Externa releases.                                                                                    |
| Your git remote               | After you `git init` / push that tree to **your** repo, that remote tracks **your** commits — it does **not** auto-update from `qiick-io/externa-core`.                                              |
| Packagist `type: project`     | Same scaffold story: Composer does **not** replace your app on later `composer update`.                                                                                                              |
| Recommended update channel    | **Git:** clone (or add `upstream`) and checkout / merge a release tag `vX.Y.Z`.                                                                                                                      |
| `php artisan externa:upgrade` | Default: after code is already on the new release — `migrate --force`, `permissions:sync`, `optimize:clear`. **No** composer/npm. Opt-in: `--sync-upstream=vX.Y.Z` can merge the official tag first. |
| Docker                        | Pull / pin a newer **GHCR** image tag; keep named volumes; then migrate inside the container (or rely on `RUN_MIGRATIONS`).                                                                          |

> **create-project ≠ update channel**
> Operators who started with Packagist and then pushed to their own GitHub/GitLab still need a deliberate code update (git tag / `upstream` merge / `--sync-upstream`, or Docker image bump). Point them here from [Packagist](/docs/packagist), [Installation](/docs/installation), [Quick Start](/docs/quick-start), and the [core README](https://github.com/qiick-io/externa-core#quick-start).

> Upgrade the host PHP runtime to **8.4.27+** or **8.5.12+** (CVEs in PHP itself, not Externa). From externa-core **1.2.2**, Composer requires `php: ^8.4.27` ([#248](https://github.com/qiick-io/externa-core/pull/248)). Official `ghcr.io/qiick-io/externa-core:1.2.2` and `latest` ship PHP 8.4.27 — re-pull and recreate containers; source builds use `--pull --no-cache`. Full notice: [externa-core discussion #247](https://github.com/qiick-io/externa-core/discussions/247).

## Version-specific notes

### 1.2.x → 1.3.0 (Unreleased)

- **Breaking — outbound webhooks / Slack SSRF** ([core#118](https://github.com/qiick-io/externa-core/issues/118) / [PR #267](https://github.com/qiick-io/externa-core/pull/267)): loopback, RFC1918, link-local, and other reserved hosts are **rejected on save and at delivery** unless you opt in. Delivery also refuses HTTP redirects. For LAN receivers (e.g. n8n on a private IP), set `WEBHOOKS_ALLOW_PRIVATE_HOSTS=true` and refresh config cache. Public HTTPS endpoints need no change — [Outbound webhooks](/docs/webhooks#ssrf-guard).
- **Breaking — upload MIME allowlist** (same PR): after the extension denylist, uploads must sniff to a type in `config/files.php` `upload_allowed_mimes`, with magic-byte checks for claimed formats. **SVG, JSON, and XML are not allowed.** Extend the allowlist only if you accept the risk — [File manager — Upload security](/docs/file-manager#upload-security).
- Also in 1.3.0 (non-breaking defaults): optional API cost / GraphQL / payload caps ([core#113](https://github.com/qiick-io/externa-core/issues/113)), API key scopes ([core#116](https://github.com/qiick-io/externa-core/issues/116)), `ai:rotate-webhook-token` ([core#207](https://github.com/qiick-io/externa-core/issues/207)). See core `CHANGELOG.md` `[Unreleased]`.

### 1.2.1 → 1.2.2

- Security patch ([#250](https://github.com/qiick-io/externa-core/pull/250)). Composer `php` constraint is now **`^8.4.27`** ([#248](https://github.com/qiick-io/externa-core/pull/248)). No new migrations. See the PHP security notice above and [discussion #247](https://github.com/qiick-io/externa-core/discussions/247) — do not treat older 8.4.x as enough.
- AI collection-import webhook: sign the **raw request body only** (`sha256=` + HMAC-SHA256). Invalid or missing signatures return **401** (was 403). Set `AI_WEBHOOK_TOKEN` in every non-local environment ([#214](https://github.com/qiick-io/externa-core/pull/214)) — [AI API](/docs/ai-api#webhook-hmac-auth).
- `externa:install` keeps an existing `APP_KEY` or generates one, runs the minimal profile without Redis, and can be re-run with `--force` ([#249](https://github.com/qiick-io/externa-core/pull/249)) — [Installation](/docs/installation#interactive-installer-externa-install).
- Docker: pin `ghcr.io/qiick-io/externa-core:1.2.2`.

### 1.2.0 → 1.2.1

- Hotfix for field renames dropping values from pending drafts and older revisions ([#218](https://github.com/qiick-io/externa-core/issues/218), [#223](https://github.com/qiick-io/externa-core/pull/223)). No new migrations.
- **Right after deploying, before the scheduler runs or anyone publishes**, repair drafts/revisions that still hold a pre-rename key: `php artisan externa:repair-renamed-field-drafts` (dry run), then `--apply`. See [Schema changes](/docs/schema-changes#repairing-older-drafts).
- Run `npm ci && npm run build` (the `vite` range is now `^8.3.5`).
- Docker: pin `ghcr.io/qiick-io/externa-core:1.2.1` (or jump straight to **1.2.2** if you are catching up).

### 1.0.x → 1.1.0

- **AI chat history schema** (`laravel/ai` ^1.0): conversations use a polymorphic participant, and messages store `steps` + `status` instead of `tool_calls` / `tool_results`. You **must** run `php artisan migrate` (included in `externa:upgrade`) before opening existing AI conversations. Back up first: [Backup & restore](/docs/backup-restore).
- Destructive AI tool actions (delete, force-delete, bulk delete, delete field, AI turn rollback) now wait for **Approve / Reject** in the chat.
- Scheduled publish: make sure `collections:process-schedules` runs on the scheduler (`php artisan schedule:work` or cron).

### 1.1.x → 1.2.0

- Run `composer install` (adds `laravel/socialite` `^5.31`), then `npm ci && npm run build`, **before** `externa:upgrade`.
- Migrations add editorial approval columns, `collections_items.board_sort` / `published_at`, and `users.oidc_sub`, and make **`users.password` nullable** (SSO-only users). Project settings also gain notifier mute list `notifier_event_types` (null = all events).
- Run `permissions:sync` (included in `externa:upgrade`) to create `can-submit-collections` / `can-approve-collections`.
- Restart Horizon and Reverb after deploy (presence, Meilisearch sync, and git-export jobs).
- Docker: pin `ghcr.io/qiick-io/externa-core:1.2.0`.
- All new integrations (Meilisearch, git export, Slack/Telegram, OIDC) are off until configured — [Environment variables](/docs/environment-variables#optional-integrations-1-2-0).

## Before you start

1. **Backup** DB + `.env` + `storage/` — [Backup & restore](/docs/backup-restore).
2. Put the site in maintenance mode if operators share the host:

```bash
php artisan down
```

3. Note current version (`composer.json` → `version` or Git tag).

## Path A — Git tag / upstream (recommended)

### A1. You cloned `qiick-io/externa-core` (or keep `origin` = Externa)

```bash
cd /path/to/externa-core
git fetch --tags origin
git checkout v1.0.0   # example — use the target release tag
composer install --no-dev --optimize-autoloader
```

Then continue at [Shared steps](#shared-steps-after-code-is-on-the-new-release).

### A2. You create-project’d, then pushed to **your** remote

`origin` is yours. Add Externa as `upstream` and merge the release tag:

```bash
cd /path/to/my-externa
git remote add upstream https://github.com/qiick-io/externa-core.git   # once
git fetch --tags upstream
git merge v1.0.0   # example — resolve conflicts if you customized app code
composer install --no-dev --optimize-autoloader
```

Vanilla trees (few or no app edits) usually merge cleanly. Heavy forks: merge/rebase carefully, keep `.env` / `storage` / DB.

Or use the assisted CLI path below instead of the manual `git` steps.

Contributors on `develop`: merge/rebase onto the target tag or release branch, then the same composer / migrate / build steps.

## Assisted sync (`--sync-upstream`)

**Opt-in only.** Plain `externa:upgrade` still does **not** pull from GitHub. Use this when you want the Artisan command to fetch the official remote and merge a release tag, then run the usual migrate / permissions / cache steps.

### What it does

1. Requires a **git** working tree (fails clearly if not).
2. Ensures remote `upstream` → `https://github.com/qiick-io/externa-core.git` (adds it if missing).
3. `git fetch --tags upstream`, verifies `refs/tags/vX.Y.Z`.
4. Lists files that would change (`git diff --name-status HEAD vX.Y.Z`).
5. Asks for confirm (“N files will be overwritten / changed — proceed with caution?”) unless `--yes`.
6. `git merge --no-edit vX.Y.Z` — on conflict, **stops** (no migrate); resolve by hand.
7. On success (or if you skipped sync): migrate + `permissions:sync` + `optimize:clear`.

### Risks / when to use

- **May overwrite customized app files** and can fail on heavy forks. Prefer Path A by hand if you have large local diffs.
- Refuses a dirty tree unless `--force` (not recommended with sync).
- Does **not** run `composer` / `npm` — install deps after code lands.
- Prefer **dry-run first**.

```bash
# Preview only — no merge, no migrate
php artisan externa:upgrade --sync-upstream=v1.0.0 --dry-run

# Apply sync (confirm prompt) then migrate / permissions / cache
php artisan externa:upgrade --sync-upstream=v1.0.0

# Non-interactive apply (CI / scripts) — still fails on conflicts
php artisan externa:upgrade --sync-upstream=v1.0.0 --yes

# Production hosts
php artisan externa:upgrade --sync-upstream=v1.0.0 --force --yes
```

> **Warning**
> Assisted sync is a convenience for mostly-vanilla trees. Customized forks should merge carefully, keep `.env`, and never blind-commit conflict resolutions.

## AI-assisted upgrade

When merge conflicts or a large diff make a human review painful, paste context into your coding agent. **Keep `.env` and secrets out of commits.** Do **not** ask the agent to force-push or blind-commit without review.

### Template prompt (per version)

Copy and fill `vOLD` / `vNEW` (example: `v1.0.0` → `v1.1.0`):

```text
I am upgrading Externa (Laravel app from qiick-io/externa-core) from vOLD to vNEW.

Context to use (paste one or more):
1) GitHub compare: https://github.com/qiick-io/externa-core/compare/vOLD...vNEW
2) And/or CHANGELOG + release notes for vNEW
3) My conflicted / customized files (paste `git status` and the conflict hunks)

Goals:
- Resolve conflicts preserving my intentional customizations
- Keep .env, storage/, and local secrets untouched — never commit them
- Prefer upstream for framework/app defaults I did not customize
- After a clean tree: composer install, npm ci && npm run build, then
  `php artisan externa:upgrade` (or migrate + permissions:sync + optimize:clear)

Constraints:
- Do not blind-commit; show the plan and diffs first
- Do not rewrite git history or force-push
- Call out breaking changes from the compare/CHANGELOG
```

After the agent proposes a resolution: review, run tests / smoke checklist, then commit yourself.

## Path B — Replace tree (vanilla create-project, no useful git history)

When you never put the site under git, or history is disposable:

1. Backup `.env`, `storage/`, and the database.
2. `composer create-project qiick/externa-core:X.Y.Z /tmp/externa-new` (pin the target version).
3. Copy `.env` (and any local customizations) into the new tree; keep the same DB and storage volume/mount.
4. Run [Shared steps](#shared-steps-after-code-is-on-the-new-release) in the new tree, then cut over the web root.

Do **not** expect `composer update qiick/externa-core` to refresh application source — the package was copied once at create-project time.

## Path C — Docker (GHCR / Compose)

Keep named volumes (`externa_*_storage`, DB volumes). Bump the image tag, then bring the stack back up:

```bash
# Quick deploy sample — pin the release you want (example: 1.2.2)
EXTERNA_IMAGE=ghcr.io/qiick-io/externa-core:1.2.2 \
  docker compose -f compose.quick.yaml pull
EXTERNA_IMAGE=ghcr.io/qiick-io/externa-core:1.2.2 \
  docker compose -f compose.quick.yaml up -d
```

Build-from-source Compose: checkout / merge the matching tag in the repo that owns `compose.yaml`, then `docker compose … up --build -d`.

If `RUN_MIGRATIONS` is off, run migrate (+ permissions) in the app container:

```bash
docker compose exec app php artisan migrate --force
docker compose exec app php artisan permissions:sync
# or: docker compose exec app php artisan externa:upgrade --force
```

Details: [Deploy with Docker](/docs/deploy-with-docker) · volumes note below.

## Shared steps (after code is on the new release)

```bash
php artisan migrate --force
php artisan permissions:sync
# Only if PermissionEnum removed names and you intend cleanup:
# php artisan permissions:sync --prune

npm ci
npm run build

php artisan optimize:clear
# production hosts that cache config/routes:
# php artisan config:cache && php artisan route:cache && php artisan view:cache

php artisan up
```

Restart queue workers / Horizon / Reverb / `pulse:work` / scheduler after deploy so they load new code.

### `php artisan externa:upgrade`

Runs the **safe automated subset** by default: `migrate --force`, `permissions:sync`, `optimize:clear`.

```bash
php artisan externa:upgrade
# production or dirty git tree:
php artisan externa:upgrade --force
```

**Does not** (by default) fetch git tags, pull from GitHub, run `composer`, or run `npm`. Get the new code first (Path A/B/C), **or** use opt-in `--sync-upstream` above. Refuses `APP_ENV=production` and a dirty git working tree unless `--force`.

| Flag                                                        | Meaning                                                                                    |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `--sync-upstream=vX.Y.Z`                                    | Opt-in: fetch `upstream` + merge tag (see [Assisted sync](#assisted-sync---sync-upstream)) |
| `--dry-run`                                                 | With `--sync-upstream`: list files only; no merge / migrate                                |
| `--yes`                                                     | Skip confirm when applying `--sync-upstream`                                               |
| `--force`                                                   | Allow production and/or dirty tree                                                         |
| `--no-migrate` / `--no-permissions` / `--prune-permissions` | Tune the post-sync subset                                                                  |

## Docker volumes

With Compose (`compose.yaml` / `compose.prod.yaml` / `compose.quick.yaml`):

- Keep named volumes (`externa_storage` / `externa_prod_storage` / `externa_quick_*`, DB volumes).
- Rebuild or pull the app image after the tag change.
- Prefer `externa:upgrade --force` (or migrate + `permissions:sync`) inside the app container when entrypoint migrate is off.

## Smoke checklist

- [ ] `GET /health/ready` → 200
- [ ] Login as an operator
- [ ] Open one collection + one item
- [ ] Queue still processes (zip or a trivial job) if you rely on async features

## Non-goals

- Silent auto-pull as the **default** of `externa:upgrade` (opt-in `--sync-upstream` only)
- Remote zip overwrite without git
- Composer create-project auto-update magic
- Cross-engine DB moves (SQLite → Postgres) — dump/restore with engine tools separately
