Operations

Markdown

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.

Mental model (read this first)

FactMeaning
composer create-projectInstall once. Copies the app tree into a new directory. It is not how you receive later Externa releases.
Your git remoteAfter 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: projectSame scaffold story: Composer does not replace your app on later composer update.
Recommended update channelGit: clone (or add upstream) and checkout / merge a release tag vX.Y.Z.
php artisan externa:upgradeDefault: 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.
DockerPull / 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, Installation, Quick Start, and the core README.

Security: PHP 8.4.27 / 8.5.12

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). 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.

Version-specific notes

1.2.x → 1.3.0 (Unreleased)

  • Breaking — outbound webhooks / Slack SSRF (core#118 / PR #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.
  • 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.
  • Also in 1.3.0 (non-breaking defaults): optional API cost / GraphQL / payload caps (core#113), API key scopes (core#116), ai:rotate-webhook-token (core#207). See core CHANGELOG.md [Unreleased].

1.2.1 → 1.2.2

  • Security patch (#250). Composer php constraint is now ^8.4.27 (#248). No new migrations. See the PHP security notice above and discussion #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) — AI API.
  • externa:install keeps an existing APP_KEY or generates one, runs the minimal profile without Redis, and can be re-run with --force (#249) — Installation.
  • 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, #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.
  • 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.
  • 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.

Before you start

  1. Backup DB + .env + storage/ — Backup & restore.
  2. Put the site in maintenance mode if operators share the host:
php artisan down
  1. Note current version (composer.json → version or Git tag).

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

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.

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

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

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.
# 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):

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 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:

# 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:

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 · volumes note below.

Shared steps (after code is on the new release)

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.

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.

FlagMeaning
--sync-upstream=vX.Y.ZOpt-in: fetch upstream + merge tag (see Assisted sync)
--dry-runWith --sync-upstream: list files only; no merge / migrate
--yesSkip confirm when applying --sync-upstream
--forceAllow production and/or dirty tree
--no-migrate / --no-permissions / --prune-permissionsTune 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