# Changelog

All notable changes to `deployStatic` will be documented in this file. See `README.md` for this project's relationship to formbridge (`http://svn.dailydata.net/svn/formbridge/trunk`) — a separate repository this tool optionally integrates with, not a dependency of it.

## [0.3.0] - 2026-09-04

Implements `ProjectDefinition.md` §10.9 (build-plan phase 12): the deploy-time form rewriter, for sites where the designer builds the form in a WordPress plugin rather than a bare Custom HTML block.

### Added

- **`rewriteFormPage`**: converts a WordPress form plugin's exported `<form>` markup into one formbridge accepts. Renames each mapped field, fixes `action`, deletes the plugin's dead hidden inputs, renames and inline-hides the honeypot, strips the plugin's now-dead `<script>` tags (matched by `id`, so unrelated jQuery/theme/bootstrap scripts are untouched), and inserts the SSI include immediately inside the `<form>` tag. Every `id`/`class`/label is left alone, which is what preserves the site's styling.
- **Three assertions**, all of which abort the deploy rather than ship a broken or silently-degraded form (§10.9.3): the `formFields` map's targets must exactly match formbridge's declared field list (catches config drift in either direction); every mapped source name must actually exist in the export (catches the plugin renumbering its own field IDs); and no visible, unmapped input may remain after rewriting (catches a designer adding a field in the plugin UI that nobody added to the map or to config — the failure mode that would otherwise ship a field with a bracket-style name, rejected for every visitor at stage 9).
- **`fetchFieldMap`**: fetches the authoritative field list for one form from the public server via `--emit-field-map` over the same `ssh` channel already used for `chown`/`--check-export`. No second copy of the field list lives on the build server.
- New `sites.conf` keys: `formFields` (the plugin-input → formbridge-field mapping; its presence is what turns the rewriter on for a site), `formbridgeFormId`, `honeypotSource`.
- **Rewriting happens on an isolated staging copy of the export, never on the export itself.** Simply Static is expected to regenerate a fresh export before every deploy, but the script does not rely on that always being true — a retry after a failed deploy, against the same unrefreshed export, must not find the plugin's field names already renamed by the previous attempt.

### Verification notes

Tested against the **real** `smartappliances.us` export — a full Simply Static output of a live WPForms contact form, not a synthetic example — 37 checks, including that every original `id`/`class` attribute survives, the honeypot's inline hiding style is present, and each of the three assertions genuinely aborts when deliberately violated (a dropped map entry, a plugin field renumbered out from under a stale map, and a designer-added field with no mapping). The export used for this verification is not included in this repository.

Two real bugs were caught by insisting on real data and re-verifying claims rather than trusting a single green run:

- **Script removal only worked on paper.** The original implementation scoped `<script>` tag removal to inside `$inner` (the form's own contents), on the assumption the plugin's scripts lived there. Checked against the real export: every WPForms/reCAPTCHA `<script>` tag is enqueued by WordPress in the page footer, **outside** the `<form>` element entirely. The original code would have silently removed nothing on any real deploy. Fixed to operate on the whole page, matched by `id` (`wpforms-*`/`recaptcha`) so unrelated site scripts are never touched.
- **Not idempotent.** The first working version wrote the rewritten form back into the export directory in place. A second deploy against the same (unrefreshed) export then failed, because the plugin's bracket-style field names it needed to find were already gone — renamed by the first run. Fixed by rewriting on a throwaway `File::Temp` staging copy instead; the pristine export is never mutated. Caught by deliberately re-running a deploy against the same export twice, which is exactly the "retry after a failure" scenario that would otherwise have broken in production.

Two smaller mistakes worth recording since they'll recur in future regex-heavy work: a hidden-input-deletion regex using an invalid `(?:(?!\/?>)[^])*?` construct compiled cleanly but matched nothing at runtime (replaced with a plain `[^>]*`, verified against real markup before trusting it again); and `cp -a SRC DST` behaves differently depending on whether `DST` already exists — `File::Temp::tempdir()` always pre-creates it, so `cp -a` copied the source *as a subdirectory* of the staging dir rather than its contents, until fixed to use the `SRC/.` idiom.

Accepted, documented gaps (harmless, not fixed): an inert `wp_localize_script()` inline `<script>` with no `id` attribute survives, since nothing calls it once the real plugin script is gone; an emptied `wpforms-recaptcha-container` `<div>` (its hidden input removed) survives for the same reason.

## [0.2.0] - 2026-09-03

Implements `ProjectDefinition.md` §10.8 groups B and C (build-plan phase 11).

### Added

- **`--checksum`** on the rsync invocation. Simply Static re-exports every file on every deploy, so rsync's default size+mtime quick-check flags everything as changed regardless of actual content — and rebuilding an unchanged file via temp-file-and-rename breaks its hardlink to the pre-deploy backup for no reason. Verified directly: a byte-identical file with only a differing mtime was rebuilt (and its hardlink broken) with `--checksum` removed, and correctly skipped (hardlink preserved) with it present.
- **`--chmod=D755,F644`** on the rsync invocation, applied during transfer rather than as a `chmod -R` pass afterward — the latter would ignore rsync's own `fpc/` exclusion and reset the honeypot/signature snippet directory's mode (the §10.6 trap).
- **Pre-deploy backup** (`cp -al` to `<siteRoot>/private/deploy-backup-<timestamp>/web`), with a **retention policy**: the newest `backupRetention` backups (default 7, overridable per site in `sites.conf`) are kept, older ones pruned after each successful backup. Pruning matches only the exact `deploy-backup-<timestamp>` naming pattern, verified not to touch any other directory under `private/`.
- **Tier-1 sentinel check** (`formPages` in `sites.conf`): before transfer, confirms the SSI include (`<!--#include virtual=`) is present in each listed exported form page. Catches a minifier having stripped it, before anything reaches the public server. Sites with no `formPages` configured skip the check rather than failing it.
- **Tier-2 audit** (`formbridgeSiteId` in `sites.conf`): after transfer and `chown`, invokes formbridge's `--check-export` over the same `ssh` channel already used for `chown`, against the deployed bytes. `--site` also serves as a drift guard — `--check-export` hard-fails if the directory it was handed doesn't match that site_id's configured `site_root`.
- **On tier-2 failure: notify, do not roll back.** The notice names the site, includes `--check-export`'s own specific failure detail, and gives the literal `rsync` command to restore from the pre-deploy backup — or, on a site's first-ever deploy (no backup exists), says so explicitly rather than pointing at a path that was never created. Exits non-zero either way, so cron mails it.
- `$formBridgeBin`, a single path constant near the top of the script (per §10.8 item 10), so the remote formbridge install location can be corrected at deploy time without hunting through the body.

### Changed

- `logmsg` renamed to `logMsg`, completing the Perl-Standards.md camelCase conformance started in 0.1.0 (`ssh_host`/`$ssh_host`).
- `sites.conf.sample` documents the three new optional keys (`formPages`, `formbridgeSiteId`, `backupRetention`), all with a worked example.
- `sites.conf` (the real one, not included in this repository) noted explicitly that neither `stargazer` nor `smartappliances` had these keys set yet, since neither has a formbridge-signed form deployed — the checks are skipped for both, not failed, until that changes. No fabricated site_id/formPages values were invented for either.

### Verification notes

The pre-deploy backup was proven by inspection, not by trusting `cp`'s exit code: a file identical between the export and the live tree was confirmed to **retain the same inode** in the live tree and the backup after a real deploy (hardlink survived, because `--checksum` correctly skipped it); a file that genuinely changed was confirmed to have **different inodes**, with the backup's copy read back and verified to hold the exact pre-deploy content. A corrected negative control (an earlier attempt used `perl -i`, which turned out to itself use rename-based replacement and so proved nothing) confirmed that a **true in-place write to the same inode** — what `rsync --inplace` would do — does corrupt the hardlinked backup, which is exactly why item 7 prohibits combining the two.

## [0.1.0] - 2026-09-03

### Added

- Initial version, promoted from a several-hours-old draft to a documented, minimally-safe tool.
- Export sanity check before rsync: refuses to run if the source directory is missing, has no `index.html`, or looks emptier than a plausible minimum — the case that previously let `rsync --delete` remove a live site rather than update it.
- Trailing-slash guard on each site's `from`, since omitting it changes what rsync considers the sync root.
- `FindBin`-relative lookup of `sites.conf`, so the script no longer depends on the caller's working directory.
- rsync and ssh output captured to the per-site log (previously went to the terminal only and was lost on an unattended/cron run).
- `README.md`, `sites.conf.sample`, and this changelog.

### Changed

- `ssh_host` / `$ssh_host` renamed to `sshHost` / `$sshHost` for camelCase consistency with this coder's Perl convention (`CommonProjectStandards/Perl-Standards.md` §1). Applies to both the script and `sites.conf`.
- Removed a dead, commented-out duplicate of the `@rsync` array construction.

### Known gaps (tracked for a later version)

- `logmsg` is not yet renamed to `logMsg` (same convention as above) — deferred, not overlooked.
- No `--checksum`, `--chmod`, or pre-deploy backup yet (rsync currently treats every file as changed on every deploy, since Simply Static re-exports everything).
- No `formPages`/`formbridgeSiteId` keys or `--check-export` integration yet — this script does not yet know about `formbridge` snippet validation.
