| @@ -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\<site>\ | |||||
| releases\ | |||||
| <release-id>\ | |||||
| 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 `<release>\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\<IIS site name> | |||||
| ``` | |||||
| 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 | |||||
| <DeployRoot>\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 `<DeployRoot>\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 `<release-id>.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. | |||||
Powered by TurnKey Linux.