diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index 951813d..b2b15d7 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -1,37 +1,235 @@ -# MVC-Starter - Deployment Guide +# IIS Deployment Guide -**Date:** 2026-03-11T11:59:39Z +## Deployment model -## Deployment Model +Production uses immutable release directories on the Windows host: -Single-site Windows IIS deployment with `public/` as the web root. +```text +C:\inetpub\deployments\\ + releases\ + \ + app\ + core\ + db\ + public\ <- IIS physicalPath points here + scripts\ + ... + shared\ + public.web.config <- machine-owned configuration + deployment-state.json +``` -## Deployment Steps +The complete repository is copied into each release. This is required because +`public/Default.asp` includes files above `public/`, including `../core` and +`../app`. IIS exposes only `\public`; sibling source, scripts, tests, +and database files are not web-root content. -1. Copy the repository to the target Windows host. -2. Configure the IIS site to point to `public/`. -3. Ensure Classic ASP is enabled. -4. Ensure URL Rewrite is installed. -5. Update `public/web.config` for the target environment. -6. Ensure the Access DB file path is valid and accessible. +Switching `physicalPath` from one release's `public` directory to another is the +atomic-ish cutover. The app pool is recycled after the switch. If the smoke test +fails, the installer restores the previous physical path and recycles again. +The script does not stop the site for the normal copy/staging phase. -## Key Runtime Config +## Why Gitea is not the runner -- `ConnectionString` -- `Environment` -- `EnableErrorLogging` -- `ErrorLogPath` -- cache and UI timing settings +The repository server is Gitea 1.11.4, which predates Gitea Actions. Treat +Gitea as the Git source only. Run `scripts/deploy-iis-git.ps1` from one of: -## Deployment Risks +- a trusted external CI worker that checks out this repository; +- a controlled operator workstation; +- a separately managed scheduled task. -- Incorrect `ConnectionString` path for `.accdb` -- Missing IIS URL Rewrite module -- Missing Classic ASP support -- File permission issues for logs or database access +The worker must have Tailscale reachability and OpenSSH key-based access to +`webserver-1`. No password, private key, database path, or connection string +belongs in this repository. -## What Was Not Found +## Files -- No Docker, Kubernetes, or container deployment setup -- No CI/CD pipeline config -- No infrastructure-as-code deployment definition +- `scripts/deploy-iis-git.ps1`: controller/CI entry point. Validates branch and + worktree state, packages the full source tree, hashes it, transfers it with + `scp`, and invokes the host installer with `ssh`. +- `scripts/install-iis-release.ps1`: elevated host-side installer. Performs + preflight, preserves machine configuration, stages/releases, configures + Classic ASP parent paths, switches IIS, smoke tests, and rolls back. + +Both scripts target Windows PowerShell 5.1 syntax. + +## One-time host preparation + +Complete these steps on `webserver-1` before the first live deployment: + +1. Join the host and CI worker to the intended Tailscale tailnet. Confirm ACLs + allow SSH only from the intended deploy identity. +2. Install and configure Windows OpenSSH Server for key-based authentication. + The deploy identity must be able to run an elevated, non-interactive + PowerShell process; choose and audit the elevation mechanism locally. +3. Install IIS, Classic ASP (`IIS-ASP` / `Web-ASP`), and IIS URL Rewrite. +4. Create the IIS site and application pool. The scripts require an existing + site and do not change its bindings, authentication, app-pool identity, or + managed runtime. +5. Ensure the site currently points at a valid `public` directory containing + its machine-specific `web.config`. On first deployment this file is copied + to `shared\public.web.config` and becomes the deployment-owned preserved + copy. +6. Put the production Access database and logs outside release directories. + Grant the existing app-pool identity only the required permissions. The + deployment scripts deliberately do not change app-pool identity or ACLs. +7. Grant the deploy identity modify permission on the chosen deployment root + and permission to update this IIS site's configuration. +8. Back up IIS configuration and production data using the host's normal backup + system before enabling unattended deployment. + +Default deployment root: + +```text +C:\inetpub\deployments\ +``` + +Classic ASP parent paths are set explicitly at the site's location in +`applicationHost.config` by `install-iis-release.ps1`: + +```text +system.webServer/asp enableParentPaths = true +``` + +This is intentional. The ASP section is commonly locked against `web.config` +overrides, so the setting is applied at the host/site level rather than added to +repository `public/web.config`. + +## Safe validation sequence + +From a standalone checkout on a Windows worker, first validate without making a +network connection: + +```powershell +.\scripts\deploy-iis-git.ps1 ` + -SiteName 'ttasp' ` + -RemoteTarget 'webserver-1' ` + -ExpectedBranch 'master' ` + -DryRun +``` + +`-DryRun` validates the local source, branch, dirty state, XML, package creation, +and argument construction. It prints remote operations but does not run `ssh` +or `scp`. + +Next run a remote read-only preflight. This does connect to `webserver-1`, copies +only the installer to a temporary directory, and checks the existing site, +app pool, Classic ASP feature (when its feature cmdlet is available), URL +Rewrite module, and availability of a preservable `web.config`: + +```powershell +.\scripts\deploy-iis-git.ps1 ` + -SiteName 'ttasp' ` + -RemoteTarget 'webserver-1' ` + -RemotePreflightOnly +``` + +For a normal deployment: + +```powershell +.\scripts\deploy-iis-git.ps1 ` + -SiteName 'ttasp' ` + -RemoteTarget 'webserver-1' ` + -ExpectedBranch 'master' ` + -BaseUrl 'http://127.0.0.1/' +``` + +Use a host header or HTTPS URL in `-BaseUrl` when the site's binding requires +one. If no URL is supplied, the installer uses the first HTTP binding's port on +`127.0.0.1`. `-SkipSmokeTest` is an explicit exception and should not be used in +unattended production CI. + +## Branch and artifact policy + +The controller defaults to `-ExpectedBranch master`, matching the repository's +actual default branch. It refuses a different branch or dirty worktree. External CI should: + +1. fetch from Gitea over its configured authenticated channel; +2. check out the exact approved commit on `master`; +3. run repository tests/static checks; +4. call the deployment script from that standalone checkout; +5. retain the commit ID, release ID, package SHA-256, and deployment output. + +Use `-AllowAnyBranch` or `-AllowDirty` only for an intentional, reviewed manual +exception. If `SourcePath` is not itself a Git root, `-AllowAnyBranch` is +required because branch provenance cannot be proved. + +A webhook receiver is not included. Gitea 1.11.4 can emit webhooks, but accepting +and authenticating them safely is infrastructure-specific. Polling or a manually +approved CI job is simpler unless an existing secured webhook runner is +available. + +## Configuration preservation + +The installer copies the current site's entire `public\web.config` to: + +```text +\shared\public.web.config +``` + +It then overwrites each staged release's repository version with that preserved +file before cutover. This preserves all machine-specific values, especially: + +- `ConnectionString` and its Access `Data Source` path; +- `Environment`; +- `ErrorLogPath` and logging flags; +- site-specific rewrite or app settings already present on the host. + +The shared file is never automatically refreshed from repository content. To +change production settings, edit the shared file under the host's normal change +control, validate its XML, and deploy or roll back. Keep a protected backup. +Do not commit production values. + +## Database migration policy + +Deployments do **not** run migrations by default. The sample `db` directory is +part of the complete release but the production connection string should point +to data outside the release tree. + +`-RunMigrations` is available only as an explicit operator choice. Before using +it: + +1. take and verify a database backup; +2. review every pending migration against the target schema; +3. schedule downtime if the Access database requires exclusive access; +4. understand that switching IIS back does not undo a database migration. + +The old script's automatic legacy `migrate_isbusiness_to_households.vbs` path has +been removed from deployment. Run one-off data repair scripts only under a +separate reviewed procedure. + +## Rollback + +List release IDs on the host under `\releases`, or use the value in +`deployment-state.json`. Then run from the worker: + +```powershell +.\scripts\deploy-iis-git.ps1 ` + -SiteName 'ttasp' ` + -RemoteTarget 'webserver-1' ` + -Rollback ` + -RollbackTo '20260914-171500-a1b2c3d4e5f6' ` + -BaseUrl 'http://127.0.0.1/' +``` + +Rollback refreshes the target release's `public\web.config` from the preserved +shared copy, switches `physicalPath`, recycles the app pool, and smoke tests. If +that smoke test fails, it restores the pre-rollback path. + +The installer reports releases older than `-KeepReleases` as retention +candidates but does not delete them automatically. Delete only after confirming +they are neither current nor required for rollback and that backups exist. + +## Failure and recovery behavior + +- Package extraction occurs in `.staging`. +- Layout and `web.config` XML are validated before promotion. +- Promotion is a directory rename within the release volume. +- IIS changes only after the final release directory exists. +- Failed smoke tests restore the prior `physicalPath`. +- Incomplete staging directories are retained for diagnosis. +- Database changes, external file writes, and machine configuration outside the + IIS path switch cannot be undone by release rollback. + +Review Windows Event Log, IIS logs, app logs, the script transcript, and +`deployment-state.json` after any failure.