Operations
MarkdownUpgrade
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)
| 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, 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=trueand 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.phpupload_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 coreCHANGELOG.md[Unreleased].
1.2.1 → 1.2.2
- Security patch (#250). Composer
phpconstraint 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). SetAI_WEBHOOK_TOKENin every non-local environment (#214) — AI API. externa:installkeeps an existingAPP_KEYor 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(theviterange 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 storesteps+statusinstead oftool_calls/tool_results. You must runphp artisan migrate(included inexterna: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-schedulesruns on the scheduler (php artisan schedule:workor cron).
1.1.x → 1.2.0
- Run
composer install(addslaravel/socialite^5.31), thennpm ci && npm run build, beforeexterna:upgrade. - Migrations add editorial approval columns,
collections_items.board_sort/published_at, andusers.oidc_sub, and makeusers.passwordnullable (SSO-only users). Project settings also gain notifier mute listnotifier_event_types(null = all events). - Run
permissions:sync(included inexterna:upgrade) to createcan-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
- Backup DB +
.env+storage/— Backup & restore. - Put the site in maintenance mode if operators share the host:
php artisan down
- Note current version (
composer.json→versionor Git tag).
Path A — Git tag / upstream (recommended)
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
- Requires a git working tree (fails clearly if not).
- Ensures remote
upstream→https://github.com/qiick-io/externa-core.git(adds it if missing). git fetch --tags upstream, verifiesrefs/tags/vX.Y.Z.- Lists files that would change (
git diff --name-status HEAD vX.Y.Z). - Asks for confirm (“N files will be overwritten / changed — proceed with caution?”) unless
--yes. git merge --no-edit vX.Y.Z— on conflict, stops (no migrate); resolve by hand.- 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:
- Backup
.env,storage/, and the database. composer create-project qiick/externa-core:X.Y.Z /tmp/externa-new(pin the target version).- Copy
.env(and any local customizations) into the new tree; keep the same DB and storage volume/mount. - 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.
| Flag | Meaning |
|---|---|
--sync-upstream=vX.Y.Z | Opt-in: fetch upstream + merge tag (see Assisted sync) |
--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-upstreamonly) - Remote zip overwrite without git
- Composer create-project auto-update magic
- Cross-engine DB moves (SQLite → Postgres) — dump/restore with engine tools separately