| @@ -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.