## Context

See proposal.md — Why. The relevant constraints are structural rather than motivational:

- The reference implementation is `hrm-api`'s `.gitlab-ci.yml`, a GitLab **shell** runner living on the dev server itself (tag `web_dev`). There is no Docker, no rsync, no SSH step: the job's working directory *is* on the target host, so "deploy" is a local `cp -Rf`.
- Trigger is a git tag, not a branch push. `hrm-api` uses `^dev-[0-9]{10}$`, where the ten digits are `yymmddHHMM` (e.g. `dev-2608221352`).
- `hrm-api` commits `.env.dev`, `.env.testing`, `.env.staging`, `.env.production` and lets CI promote the right one to `.env`. Its `.gitignore` ignores `.env` only. `tnx-pos` has the same `.gitignore` shape, so the same trick works unchanged.
- `tnx-pos` is Laravel `5.6.*` with `vendor/` committed (7146 tracked files) plus `composer.phar` and a `patch_package_manifest.php` hook wired into `post-autoload-dump`. That patch exists to make Composer 2's `installed.json` format readable by Laravel 5.6's `PackageManifest`, which tells us dependency installation on this project is fragile and deliberately pinned.
- `public/css` and `public/js` are committed, so there is no asset build stage to port.

## Goals / Non-Goals

**Goals:**

- Reproduce `hrm-api`'s dev deploy shape closely enough that anyone who knows one can read the other.
- Deviate only where the target codebase forces it, and record each deviation.
- Keep the whole change to files that do not yet exist, so nothing pre-existing is touched.

**Non-Goals:**

- Improving on the reference. No artifact retention, no rollback slot, no health check, no `--force` hardening beyond what `hrm-api` does. Parity is the point.
- Verifying the pipeline against the live runner. The user has explicitly deferred a real test run.

## Decisions

**Copy the reference rather than modernize it.** The instruction was explicit: near-copy, config-only changes. A "better" pipeline (artifact-based deploy, atomic symlink swap, zero-downtime) would be more correct in isolation but would leave two sibling projects with two mental models. Alternative considered and rejected: rewriting as a `deploy:` stage with `environment:` and GitLab-managed variables.

**Drop `composer install` entirely instead of keeping or replacing it.** Three options were on the table: (a) keep `composer install --no-interaction` as in `hrm-api`; (b) downgrade to `composer dump-autoload -o`; (c) drop it. Chose (c). `vendor/` is committed with autoload files already generated, so the job has everything it needs at checkout. Option (a) re-resolves a Laravel 5.6 dependency tree on a host provisioned for a Laravel 6 project — the single most likely way to break a first deploy. Option (b) still invokes Composer, still fires `post-autoload-dump`, still re-runs the manifest patch, and buys nothing that the committed autoloader does not already provide.

**Bake `APP_KEY` into `.env.dev` rather than generating it in CI.** This mirrors `hrm-api`, and the reason it mirrors it is sound: the job wipes `$DEV_DIR/src` on every run, so a CI-side `key:generate` would mint a fresh key each deploy and invalidate every existing session and encrypted column. A committed key is a secret in the repo, but it is a *dev* key on an internal host, and the reference project has made the same trade for years. The `after_script` `APP_KEY` grep is kept verbatim as the sanity check.

**Prune Artisan steps by verified absence, not by guess.** `passport:keys`, `permissions:autoload`, `administrative-units:import` are dropped because Passport is absent from `composer.json` and `app/Console/Commands/` contains exactly one unrelated command (`CustomerOrderSummaryLogging.php`). Each dropped step would otherwise fail the job outright.

**Add `config:clear` on top of the reference's `cache:clear`.** `hrm-api`'s dev job runs only `cache:clear`; its staging job runs both. Since `.env` is swapped in on every deploy, a stale `bootstrap/cache/config.php` would silently mask the new values, so the staging behavior is the correct one to inherit here.

**Keep plain `migrate`, not `migrate --force`.** `.env.dev` sets `APP_ENV=local`, and Laravel 5.6 only prompts for confirmation when the environment is `production`. Plain `migrate` therefore runs non-interactively and matches the reference exactly.

**Tag by hand instead of porting `deploy/*.sh`.** The reference's `npm run tag:dev` wraps `npx @w3suga/w3s-cli dev-tag`, and `deploy/version.sh` additionally needs a `version` field that `tnx-pos`'s `package.json` does not have. For a dev-only scope the equivalent is one shell command, so the three helper scripts and the `package.json` script entries are all omitted.

## Risks / Trade-offs

**PHP 8 on the shared dev host → job fails at the first `php artisan` call.** Laravel 5.6 does not run on PHP 8; `hrm-api` requires `^7.3|^8.0`. If the host serves `hrm-api` under PHP 8.x, `tnx-pos` needs its own PHP-FPM 7.x pool and a CLI `php` resolving to 7.x for the runner user. → Confirm the runner's `php -v` before the first tag; this is the single most likely failure and it is server-side, not repo-side.

**The `web_dev` runner may not be registered for this project.** `tnx-pos` lives under `dev/small-projects/`, `hrm-api` under `dev/sugahrm/`. A runner scoped to the latter group will leave the job queued forever. → Check runner availability in the project's CI settings before tagging.

**`sudo rm -rf` requires passwordless sudo for the runner user.** Inherited verbatim from the reference, which implies the dev host is already configured this way — but that was configured for a different project path. → Verify sudo rights cover `/var/www/html/tnx_pos_2026`.

**`storage:link` after a full wipe.** Laravel 5.6 throws if `public/storage` already exists. The job removes `$DEV_DIR/src` wholesale each run and `public/storage` is gitignored, so the link is always created fresh. This works, but it also means uploaded files under `storage/app/public` are the only thing standing between a deploy and data loss — they live outside `src` only if storage is symlinked out. → Worth confirming on the first deploy; not blocking.

**A committed `APP_KEY` and DB password.** Accepted deliberately, as above. If dev credentials are ever reused on a reachable host this becomes a real exposure.

**Full wipe means no rollback.** `rm -rf $DEV_DIR/src` destroys the previous deploy before the new one lands, so a bad deploy is recovered by re-tagging an older commit, not by flipping back. Acceptable for dev; would not be for staging or production.

## Migration Plan

There is nothing to migrate — no pipeline exists today. Rollout is: land the two files, complete the server-side prerequisites listed in proposal.md — Impact, then push the first `dev-` tag and watch the job. Rollback is deleting the tag and, if the job already ran, re-tagging a known-good commit.

## Open Questions

- Exact vhost hostname and port for `APP_URL`. The value written into `.env.dev` follows the `hrm-api` dev convention; it affects generated absolute URLs only, and can be corrected in place without touching the pipeline.
- Whether the dev database credentials should be project-specific rather than the shared dev account inherited from the reference. Does not change the approach or the task breakdown.
