# Staging deployment: setup and everyday use

This guide explains how to deploy Conjugate's Laravel backend to OVH using GitHub Actions, SFTP and an authenticated HTTPS runner. No DeployHQ, SSH shell or deployment cron is needed.

**Scope: staging only. Do not change production's files, database, secrets or Multisite root.** The existing production workflow still uses cron; extending it to HTTPS is a separate task.

Written on 9 September 2026, after staging `health` and a deployment with HTTP verification passed. The observed staging PHP version was `8.2.31`; the matching GitHub setting is `8.2`. Recheck these settings when reusing this guide in the future.

**Staging already works? Go straight to [Everyday use](#everyday-use).** Do not recreate its `.env`, regenerate its application key or move its domain back to the temporary root.

- [One-time setup](#one-time-setup)
- [Everyday use](#everyday-use)
- [Common errors](#common-errors)
- [Reusable coding prompt](#reusable-coding-prompt)
- [Technical reference](deployment.md)
- [Database migrations and key initialization](staging-database.md)

## How it works

GitHub builds the selected commit and its Composer packages, uploads them through SFTP, then calls the HTTPS runner to execute that specific job. A successful upload alone is not a successful deployment: the final job result must say `succeeded`.

The code version and destination are separate choices:

| Workflow field | Meaning |
| --- | --- |
| **Use workflow from: `main`** | Use the trusted deployment tooling on the default branch. |
| **Environment: `staging`** | Send the operation to staging, not production. |
| **Ref: `main`, a tag or a commit SHA** | Choose which application code to build. A SHA is Git's exact commit identifier. |

Selecting `main` does **not** select production. You do not need a branch named `staging`. This workflow is manual: pushing or merging code does not trigger it. Review any older automatic deployment integrations separately so that a merge cannot accidentally deploy production.

## One-time setup

### 1. Put the deployment code on GitHub

Repository: [heleanazoubrowska/Conjugador-Backend](https://github.com/heleanazoubrowska/Conjugador-Backend).

The implementation already exists in this backend project:

| Files | Purpose |
| --- | --- |
| [.github/workflows/ovh-backend.yml](../.github/workflows/ovh-backend.yml) | The manual **OVH backend** workflow. |
| [scripts/deploy/](../scripts/deploy/) | Build/transfer scripts, private worker, HTTP handler, public bridge and SFTP dependency. |
| [tests/Deployment/](../tests/Deployment/) | Transfer, deployment and HTTP regression tests. |

Publish the deployment files to the repository's default branch, currently `main`. Keep unrelated work separate. If the implementation is missing in a future checkout, use the [coding prompt below](#reusable-coding-prompt); uploading only the workflow YAML is not enough.

You need repository access to manage Actions settings and run workflows, plus your own OVH Manager/FileZilla access. The manual workflow must exist on the default branch for GitHub's **Run workflow** button to be available. [GitHub manual-workflow instructions](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow).

### 2. Add GitHub variables and secrets

Open **Repository → Settings → Secrets and variables → Actions**. Use **repository-level** settings; this implementation does not require a GitHub Environment named `staging`. [GitHub secret settings](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets).

Under **Variables → New repository variable**, add:

| Name | Value for this staging setup |
| --- | --- |
| `OVH_TRANSFER_PROTOCOL` | `sftp` |
| `OVH_TRANSFER_HOST` | `ftp.cluster121.hosting.ovh.net` |
| `OVH_TRANSFER_PORT` | `22` |
| `STAGING_TRANSFER_USER` | `taxiboucmy` |
| `STAGING_DEPLOY_PATH` | `/home/taxiboucmy/apidev.appconjugate` |
| `OVH_PHP_VERSION` | `8.2`, confirmed against the web runtime in step 5 |

Copy the full **Remote site path while connected through SFTP** in FileZilla. It is a path, not a URL: do not paste `sftp://username@host/...`. A path obtained through FTP can differ from the SFTP path. Confirm the hostname actually used by the working connection; do not substitute another OVH cluster without verifying it.

Under **Secrets → New repository secret**, add:

| Name | What to put there |
| --- | --- |
| `STAGING_TRANSFER_PASSWORD` | The account password used by the working SFTP connection. |
| `OVH_TRANSFER_KNOWN_HOSTS` | A verified, complete OpenSSH server-key entry for the configured host and port. |
| `STAGING_RUNNER_TOKEN` | A new independent random token generated as described below. |

No production-specific secret is needed to run staging. Existing `OVH_*` settings are also referenced by the production workflow, so review shared-setting changes rather than changing them casually.

For `STAGING_RUNNER_TOKEN`, run this on **your own computer**:

```bash
openssl rand -hex 32
```

Copy the resulting 64 lowercase hexadecimal characters directly into the GitHub secret. This is **not** the SFTP password, database password or Laravel `APP_KEY`. Do not put it in Git, chat, a URL or the application `.env`. The installer stores only a one-way digest on OVH.

The SFTP known-hosts entry has this shape; the placeholder is not a usable key:

```text
ftp.cluster121.hosting.ovh.net ssh-ed25519 REPLACE_WITH_VERIFIED_BASE64_PUBLIC_KEY
```

A hostname or `SHA256:...` fingerprint alone is insufficient. A developer can collect the public key without SSH shell access using:

```bash
ssh-keyscan -t ed25519 -p 22 ftp.cluster121.hosting.ovh.net
```

**Collection is not verification.** Before trusting the collected key, verify its fingerprint against an independently trusted connection record or with OVH. Do not bypass host-key checking to fix a mismatch. [OpenSSH security guidance](https://man.openbsd.org/ssh-keyscan.1).

### 3. Prepare the private staging environment file

In FileZilla, inside `/home/taxiboucmy/apidev.appconjugate`, create `shared/storage` and `releases` if they do not already exist. Create this file privately:

```text
/home/taxiboucmy/apidev.appconjugate/shared/.env
```

Use [.env.example](../.env.example) as the application-wide checklist. These are the important starting values; replace every placeholder before deploying:

```dotenv
APP_NAME="Conjugate Staging"
APP_ENV=staging
APP_DEBUG=false
APP_URL=https://apidev.appconjugate.com
APP_KEY=base64:REPLACE_WITH_GENERATED_BASE64_KEY

DB_CONNECTION=mysql
DB_HOST=taxiboucmydev.mysql.db
DB_PORT=3306
DB_DATABASE=REPLACE_WITH_ACTUAL_STAGING_DATABASE_NAME
DB_USERNAME=REPLACE_WITH_ACTUAL_STAGING_DATABASE_USER
DB_PASSWORD="REPLACE_WITH_STAGING_DATABASE_PASSWORD"

CACHE_DRIVER=file
SESSION_DRIVER=file
SESSION_COOKIE=conjugate_staging_session
QUEUE_CONNECTION=sync
MAIL_MAILER=log
```

For a **new staging environment only**, generate the application key on your computer with `openssl rand -base64 32`, then place its output after `base64:` in `APP_KEY`. Keep that key unchanged across deployments. It is separate from the runner token generated in step 2.

Alternatively, leave `APP_KEY=` blank for a genuinely new environment and run GitHub's **initialize-key** operation after installing the updated runner and passing health, but before the first deploy. It creates the key privately on OVH and never replaces an existing valid key. Do not leave a placeholder value when using this operation. See the [database and key setup guide](staging-database.md).

The `*.mysql.db` address is a **database host**, not necessarily the database name or username. Obtain the actual staging DB details from OVH. Do not use production credentials or set `DATABASE_URL`/`DB_URL` overrides. Keep `APP_URL` on HTTPS, without `/api` or `/runner-public`.

Use staging/sandbox credentials for external services. Do not reuse production mail, notifications, payments, queues or storage destinations unintentionally. Keep secrets on OVH; never commit this `.env`. Restrict its permissions to the hosting account, typically `600` when PHP runs as that account.

**Why `shared`?** It preserves `.env`, uploads, keys, logs and other storage across releases. Each release links to those paths, so Laravel still sees its usual `.env` and `storage`. Nothing is shared with production. Existing staging data must be backed up, not overwritten by this setup.

### 4. Install the runner

Open **GitHub → Actions → OVH backend → Run workflow** and choose:

- **Use workflow from:** `main`
- **Environment:** `staging`
- **Operation:** `install-runner`

Leave other fields at their defaults for this operation. A successful installation creates/updates `.deploy` and `runner-public` under the staging folder. It does **not** install Laravel, run migrations or create a cron task.

If an older CLI-only runner was installed, run this step again to install HTTPS support. Do not update the runner while a job is executing. If you previously configured a staging deployment cron, disable it after any active job has finished before switching execution methods.

### 5. Point the temporary website root and test health

In **OVH → Hosting plans → your hosting → Multisite**, edit **only `apidev.appconjugate.com`**. Set its **Root folder** to:

```text
apidev.appconjugate/runner-public
```

Keep HTTPS enabled and allow time for the setting to take effect. The Multisite field is relative to the hosting account, unlike the absolute SFTP path. Never point it at `shared`, `.deploy` or the whole `apidev.appconjugate` folder. [OVH root-folder instructions](https://docs.ovhcloud.com/en/guides/web-cloud/web-hosting/multisites-modify-domain).

In GitHub, start a new workflow with **Environment: staging → Operation: health**. The detailed transfer-step log should show:

- `status: ready`, `environment: staging`, `protocol: 1`.
- `zip_available: true` and `curl_available: true`.
- `environment_file_exists: true` if step 3 is complete.
- The actual PHP version. For `8.2.31`, set GitHub's `OVH_PHP_VERSION` to **`8.2`**, not `8.2.31`.

Health checks the runner, not Laravel's database or application features. A normal visit to the temporary homepage can return 403/404. The runner endpoint also rejects browser requests without its authentication header; use the workflow to test it.

### 6. Perform the first deployment — verification OFF

Start a **new Run workflow**, with these values:

| Field | First-deployment value |
| --- | --- |
| Use workflow from | `main` |
| Environment | `staging` |
| Operation | `deploy` |
| Ref | The intended branch/tag/commit; `main` is fine for initial testing |
| Migrate | Check only if the staging database needs the reviewed migrations |
| **Verify public URL (disable only for first deployment before changing web root)** | **Unchecked** |
| Request ID | Blank; a new ID is generated |
| Show output | Unchecked |

Leave Artisan arguments at their default; they are ignored for `deploy`.

**The Verify public URL checkbox defaults to checked. You must uncheck it for this first deployment.** It is not a repository variable or an `.env` setting. In **manage → Validate settings → expand Run python3… → env**, confirm:

```text
DEPLOY_ENVIRONMENT: staging
DEPLOY_VERIFY_HTTP: false
```

Use **Run workflow**, not **Re-run jobs**, when changing inputs. The website still serves the bootstrap folder, so the final Laravel website check cannot pass yet. Unchecking that check does not disable HTTPS authentication or certificate verification.

Wait for the final result **`succeeded` on staging**. Save the request ID and the `revision` (full commit SHA) from the transfer-step JSON. Do not switch website roots if the job fails or its outcome is unknown.

### 7. Switch to Laravel's public folder

Only after the first successful deployment, change **the staging domain's** Multisite Root folder to:

```text
apidev.appconjugate/current/public
```

Use the literal name **`current`**, not a release ID. The worker maintains this link for you. Keep the rest of the installation, including `shared`, in place; do not drag or move release files manually.

Once OVH has applied the change, run **health** for staging again. Expect `ready` and `current_exists: true`.

### 8. Verify a normal deployment — verification ON

Run **deploy** for **staging** again, using the **same full `revision` SHA** saved in step 6 as `ref`. Keep **Migrate unchecked** and now **check Verify public URL**.

Expect `status: succeeded` and `http_verified: true`. The verification checks the release identifier and Laravel's homepage. The setup is now complete, but application behavior and reference data still need testing.

| Stage | Multisite Root folder | Verify public URL |
| --- | --- | --- |
| Before the first release exists | `apidev.appconjugate/runner-public` | Unchecked for the first deployment |
| After the first release succeeds | Switch to `apidev.appconjugate/current/public` | Checked for the verification deployment |
| Every subsequent deployment | Leave `apidev.appconjugate/current/public` | **Keep checked** |

Once `current` exists, the worker refuses deployments with verification disabled. Never delete `current` to bypass this protection.

## Everyday use

Always use **Actions → OVH backend → Run workflow**, **Use workflow from: main**, **Environment: staging**. No reinstall or Multisite change is needed for ordinary deployments.

### Deploy a code change or new package

Commit/push the intended code, choose **Operation: deploy** and its `ref`, and keep **Verify public URL checked**. Enable **Migrate** only when you intend to apply pending migrations.

Adding a package means running Composer locally, then committing **both `composer.json` and `composer.lock`** before deploying. During `deploy`, GitHub runs `composer install` using the locked versions and uploads the resulting `vendor` directory. Development-only packages in `require-dev` are excluded. Laravel package discovery runs on OVH. Composer is not run through Artisan on the server.

### Run Artisan or migrations

Choose **Operation: artisan** and enter a JSON array in **Artisan arguments**, for example:

| Arguments | Purpose |
| --- | --- |
| `["migrate:status"]` | Inspect which migrations have run. |
| `["migrate", "--force"]` | Apply pending migrations to staging. |
| `["cache:clear"]` | Clear the configured application cache. |

`artisan` uses packages already installed in the current release; `migrate` changes the database, not Composer dependencies. In the `artisan` operation, it is the argument array that selects migrations, not the separate deploy-only Migrate checkbox. Do not use destructive commands such as `migrate:fresh` on data you need to keep. Long-running queue workers and interactive shells are not supported.

The [database setup guide](staging-database.md) explains the migrations added from the production schema. Deploy the commit containing them before attempting to run them through Artisan.

### Retrieve status or command output

Choose **Operation: status** and put the job's ID into **Request ID**. Blank means the last submitted job; `runner` means live staging health.

To see Artisan output, enable **Show output in this status run**. The initial Artisan execution and HTTP responses omit that output. Only request output you are comfortable exposing in repository workflow logs; never print credentials.

### Test the application

Point a staging build of the app at `https://apidev.appconjugate.com/api`, then test login and conjugations. Migrations may create tables without populating verb/reference data; use reviewed seeders or a sanitized import if needed. Do not point the production app at this test environment.

## Common errors

| Message or symptom | What to check |
| --- | --- |
| Workflow missing / no Run workflow button | Publish the workflow **and its scripts** to the default branch; confirm Actions is enabled and you have access to run it. |
| `DEPLOY_PATH must be an absolute transfer path` | Set `STAGING_DEPLOY_PATH` to the full SFTP path, starting with `/`, not a FileZilla URL. |
| Missing/mismatched known-hosts entry | Use a complete verified key for the actual hostname/port. Never bypass verification. |
| Environment directory missing/inaccessible | Copy the Remote site path from an **SFTP**, not FTP, connection. |
| `APP_URL must be an HTTPS base URL` | Edit **OVH `shared/.env`**: `APP_URL=https://apidev.appconjugate.com`. Do not append `/api`. |
| `HTTP verification failed; previous code restored` while using `runner-public` | Inspect the failed run's `DEPLOY_VERIFY_HTTP` value. If `true`, start a new first-deployment run with the checkbox unchecked. Migrations selected in that failed run completed before this check and were not reversed; do not blindly rerun them. |
| HTTP verification fails after switching to `current/public` | Keep verification enabled. Check the effective web root, HTTPS and private Laravel logs; health alone does not prove Laravel's homepage works. |
| `HTTP checks can only be disabled for the first deployment` | An active release exists. Leave the domain on `current/public` and enable Verify public URL. |
| Health returns 403 | Check the runner token, installation, HTTPS and web root. A browser request without the token is expected to fail. |
| PHP mismatch / Composer build failure | Match `OVH_PHP_VERSION` to web PHP major.minor; inspect package compatibility and required extensions. Do not bypass platform requirements. |

**Timeout, interrupted or uncertain result:** do not immediately deploy again. Run `status` with the **same ID**; it retrieves private results through file transfer even if HTTP is unavailable. A killed process can leave a `running` report and a marker in `.deploy/processing`. Inspect health, private logs, active code and database state before any recovery. Do not delete processing markers or replay jobs without confirming that no process remains active and reviewing what completed. Code rollback never reverses migrations.

The runner executes inside an HTTP request. OVH documents PHP execution limits of 120 seconds without PHP-FPM and 165 seconds with it; other hosting limits can also apply. It is not an unlimited background worker. [OVH runtime limits](https://docs.ovhcloud.com/en/guides/web-cloud/web-hosting/hosting-technical-specificities).

See the technical reference for [token rotation](deployment.md#staging-urls-optional-direct-use) and [storage cleanup and recovery](deployment.md#failure-recovery-and-storage).

## Reusable coding prompt

Copy the following prompt into your coding assistant **with the backend repository open**. It recreates/restores the coding part; it cannot recover passwords, GitHub Secrets, `.env` contents or OVH settings. Those must be configured privately using the steps above. If hosts, paths, PHP or the default branch have changed, update that context before reusing it.

```text
Implement or restore Conjugate's STAGING deployment system in this Laravel
backend repository. Production must remain unchanged. First read repository
instructions, inspect Git status, existing deployment tooling and tests, and
reuse/repair what is already present. Prefer recovering existing implementation
from Git history over rewriting it. Preserve unrelated work. Use a separate
deployment branch/worktree if needed; do not reset another task's work.

Context to verify before implementation:
- Repository: https://github.com/heleanazoubrowska/Conjugador-Backend
- Default branch: main. A separate staging branch is optional.
- OVH shared hosting provides SFTP, but no SSH shell is available.
- SFTP host: ftp.cluster121.hosting.ovh.net; port: 22; user: taxiboucmy.
- Staging transfer root: /home/taxiboucmy/apidev.appconjugate.
- Staging website: https://apidev.appconjugate.com; API base adds /api.
- Staging database HOST: taxiboucmydev.mysql.db.
- Database name, user and password must be supplied privately, never guessed.
- Previously tested web PHP: 8.2.31; GitHub build setting: 8.2. Recheck today.
- Production lives in api.appconjugate and must not be modified or deployed.

Desired user interface:
1. A manual GitHub Actions workflow called "OVH backend" in
   .github/workflows/ovh-backend.yml, using workflow_dispatch, not automatic
   deployment on push/merge. Preserve existing production behavior if present.
2. Use trusted tooling from the default branch and separately check out the
   requested application branch/tag/full SHA. Selecting main is not selecting
   production. Disable checkout credential persistence; use minimum permissions.
3. Support staging operations: install-runner, health, deploy, artisan, status,
   initialize-key (create a missing key only for a new staging environment).
   Inputs include ref, migrate (default false), verify_http (default true),
   artisan_arguments as a literal JSON array, request_id and show_output.
4. Serialize operations by environment without cancelling a running migration.
   Keep production on its existing mode; HTTPS administration is staging-only.

Settings contract:
- Repository variables: OVH_TRANSFER_PROTOCOL, OVH_TRANSFER_HOST,
  OVH_TRANSFER_PORT, STAGING_TRANSFER_USER, STAGING_DEPLOY_PATH, OVH_PHP_VERSION.
- Repository secrets: STAGING_TRANSFER_PASSWORD, OVH_TRANSFER_KNOWN_HOSTS,
  STAGING_RUNNER_TOKEN. The token is an independent random 64-character
  lowercase hex value, not an SFTP password, APP_KEY or database password.
- Derive DEPLOY_EXECUTION=https for staging and DEPLOY_VERIFY_HTTP from its
  boolean workflow input; serialize false as JSON false, not a truthy string.
- Use contexts valid at their workflow location. In particular, do not use
  runner.temp in job-level env; use github.workspace or a supported step context.

Build and transfer:
- Reuse scripts/deploy/build.sh, transfer.py, requirements-sftp.txt, worker.php,
  http.php and bridge.php where present. Choose compatible dependency versions
  after checking current primary documentation, not by blindly copying old pins.
- Build committed application code and locked Composer dependencies on GitHub
  with PHP matching OVH. Use composer install --no-dev --no-scripts and verify
  platform requirements; ship vendor. Run package:discover on OVH, not CI.
- Exclude .env files, persistent storage, secrets, local files and generated
  Laravel caches. Reserve public/_deploy.php for trusted deployment tooling;
  the selected application archive must not replace the management bridge.
- Use SFTP with verified known_hosts and password authentication. No SSH shell,
  SSHClient.exec_command, automatic host-key acceptance or insecure downgrade. If existing
  FTPS support is retained, verify TLS; reject plain FTP for staging HTTPS setup.
- Validate the selected root and environment. Upload a unique ZIP then its job
  JSON atomically using temporary files and rename. Never queue a partial ZIP.

Server layout and execution:
- Use staging-root/.deploy for private tooling, packages, queue, processing
  markers and results; protect it from direct HTTP access.
- Keep shared/.env and shared/storage persistent and outside the web root.
  Extract into releases/<request-id>; link .env/storage; atomically switch
  current to a validated release. Never overwrite an existing unmanaged app.
- Validate archive checksum, size, paths, symlinks, revision and PHP metadata.
- Before migrations/activation, validate actual staging Laravel configuration:
  APP_ENV=staging, APP_DEBUG=false, the staging MySQL host, an existing APP_KEY,
  HTTPS APP_URL, explicit DB settings and successful DB connectivity.
- Preserve APP_KEY. Do not add config:cache/optimize while application code uses
  env() outside configuration. Block incompatible commands and --env overrides.
- Provide a separate staging-only initialize-key job before the first release.
  Validate the private real .env and staging target, generate a secure 32-byte
  base64 key only when missing/blank, preserve valid existing keys byte-for-byte,
  refuse missing keys on active applications, and never expose the key in logs
  or responses. Keep ordinary remote artisan key:generate blocked.
- Invoke Artisan through Laravel's console kernel with literal arguments, not
  shell commands. Process one job per PHP request to isolate release state.
- Use an exclusive filesystem lock and persistent job reports. Execute exactly
  the requested uploaded ID. Repeated POSTs for a recorded result must return
  the result without rerunning successful, failed or interrupted work.
- Retain interruption markers and block further execution until reviewed.
  Never automatically retry a migration. Retain releases/results for recovery;
  do not auto-delete an existing installation or persistent files.

Staging HTTPS interface:
- Use a standalone bridge that does not depend on Laravel being installed or
  healthy. Install runner-public/_deploy.php for first-time bootstrap, and
  inject the equivalent public bridge into each staging release.
- Authenticate every action before execution using HTTPS and the header
  X-Conjugate-Deploy-Token. Store only a SHA-256 token digest privately on OVH;
  compare securely. No tokens in URLs, no unauthenticated command endpoints,
  no trusting arbitrary forwarded-protocol headers, no direct worker HTTP access.
- GET /_deploy.php?action=health: readiness, PHP/extensions; no queued execution.
- GET /_deploy.php?action=status&id=REQUEST_ID: redacted status for one job.
- POST /_deploy.php?action=run: accept only JSON {"id":"REQUEST_ID"}, referring
  to a previously uploaded job. Never accept commands or code through HTTP.
- GitHub checks readiness, uploads and triggers the job, then reports success
  only for status=succeeded. Validate HTTPS certificates and response identity,
  refuse redirects and never automatically retry POST. Save the request ID
  before execution so an operator can inspect ambiguous outcomes through SFTP.
- Hide console output from HTTP responses. Expose private Artisan output only
  through an explicit status/show_output request, with a warning about secrets.
- Respect OVH web-request limits. Do not claim unlimited/detached execution.
  A forgotten staging cron must not drain HTTPS jobs automatically.

Bootstrap sequence to preserve and document:
1. Configure GitHub settings and staging shared/.env privately.
2. install-runner uploads tooling, not Laravel. No staging cron is needed.
3. Temporarily point ONLY the staging domain at apidev.appconjugate/runner-public.
4. Run health; confirm PHP/extensions and set matching build PHP. For a new
   environment with blank APP_KEY, run initialize-key before deploying Laravel.
5. First deploy: verify_http=false, migrations only if explicitly selected.
6. After succeeded, change staging root to apidev.appconjugate/current/public.
7. Run health, then deploy the same SHA with verify_http=true and migrate=false.
8. Require verification on subsequent releases. Check the release identifier
   and Laravel homepage. Restore prior code if verification fails; never claim
   this also reverses database migrations. Do not switch production's root.

Verification and handoff:
- Add/reuse offline tests for transfer settings, real host-key parsing, secure
  TLS/no redirects, safe ZIPs, boolean false, upload-before-queue, .env/storage
  preservation, first-release verification bypass, later-release protection,
  rollback, authentication/method checks, targeting, idempotency, locks,
  interruption, output redaction and production isolation.
- Test HTTP bootstrap without Laravel and real Laravel Artisan over loopback
  where dependencies are available. Never contact a real database in tests.
- Run actionlint (not just a YAML parser), Python tests, PHP syntax checks,
  worker.test.php and http.test.php as available. Report skipped checks honestly.
- Update the beginner staging guide and technical reference. Separate the
  one-time bootstrap from everyday usage and show the exact checkbox labels.
- Implement and test locally. Do not push, change GitHub/OVH settings, deploy,
  run live migrations or touch production without separate approval. Ask only
  for missing non-secret information; tell me where to enter secrets privately.
- Summarize changed files, test results and the next manual setup step. Do not
  claim staging is live solely because local tests or uploads pass.
```
