RINGby Ringwood

09 — Build, Deploy, Release#

Who this is for. Anyone producing a build, cutting a package, or reasoning about what a plant machine actually receives.

What you'll learn. The build mechanics and the three traps in them; the shape of a release package and the provenance guarantees baked into it; how the verifier works; and where the install, rollback and remote-access procedures live — this chapter summarizes their shape, never their steps.

This chapter does not contain a procedure. Cutover, install, rollback and remote access are owned by the runbooks named in docs/INDEX.md. Executing this book instead of a runbook would be a mistake.


9.1 Build mechanics#

The toolchain and the commands are in chapter 08 §8.1 and CONTRIBUTING.md. What matters here are the three properties that make this build different from a modern SDK build.

1. Restore is packages.config-style. nuget restore Ring.sln, not dotnet restore. Packages land in packages/ at the repository root, and both csproj files reference them by relative HintPath with a pinned version — which is why a package upgrade is a csproj edit, not just a packages.config edit.

2. Nothing is globbed. Every .cs, .xaml and image is hand-registered, and ProjectMembershipGuardTests fails otherwise (chapter 08 §8.2).

3. The build reaches into the output directory. Two csproj targets, both of which bite in practice:

Target Behaviour
CopyAppSettingsToOutput (Ring/Ring.csproj:1985, AfterTargets="Build") Copies Ring/Config/appsettings.json over bin\<cfg>\Config\appsettings.json whenever the source is newer. Anything the running app wrote there — PLC IP, ReadOnlyMode, SMTP credentials, cost rates — is reverted
RemoveLocalAppSettingsFromRelease (:1995-1999, Release only) Deliberately keeps an existing appsettings.local.json in the output and emits a warning. /p:PurgeLocalConfig=true is the opt-in that deletes it; /p:SuppressLocalConfigWarning=true silences the warning

The .local.json overlay is copied into the output only for Debug, and only if it exists in Ring/Config/ (Ring/Ring.csproj:1881). Keep your overlay there — it is gitignored — not hand-placed in bin\, or a clean/rebuild takes it with the rest of the output.

**Consequence to internalise: re-check bin\<cfg>\Config\ after every build.** scripts/Launch-RingDemo.ps1 re-asserts its overlay on every launch for exactly this reason. And a Release package **can silently carry a developer's bench PLC IP** if nobody reads the warning — which is why scripts/Preflight-FieldKit.ps1 and the cutover config read-back exist, and why WriteSurfaceRegisterTests is explicit that it cannot pin the deployed overlay (WriteSurfaceRegisterTests.cs:1085-1090).

For a real plant install, fill in Ring/appsettings.production.template.json and deploy it as Config\appsettings.json; with Production.Enabled true, validation requires an absolute DB path and an absolute backup directory (ConfigurationValidator.cs:106, 112), plus the rest of the strict-profile checks in chapter 02 §2.3.


9.2 The release package#

scripts/New-ReleasePackage.ps1 produces Ring-<Configuration>-<timestamp>.zip plus a sidecar …sha256.txt, under artifacts/release by default. The authoritative description of the contract is docs/production-readiness/RELEASE_PACKAGE_CONTRACT.md; what follows is the shape, read out of the script.

Layout#

Ring-<cfg>-<timestamp>/
  App/                              the built application tree
  Install/                          only with -IncludeKiosk
    Install-RingKiosk.ps1
    Install-RingAutoRestart.ps1
    Install-RingWerDumps.ps1
    Install-DesktopLaunchers.ps1
    README-KIOSK.txt
  12_INSTALL_RUNBOOK.md
  13_RS3000_ROLLBACK.md
  STARCH_SYSTEM_CUTOVER_RUNBOOK.md
  CUTOVER_RUNBOOK.md
  release-manifest.json

The runbooks ship inside the package (New-ReleasePackage.ps1:221-233), so the machine that has the software also has the procedure for installing it, cutting over and rolling back. CUTOVER_RUNBOOK.md is staged unconditionally, because the kiosk README references it.

The four kiosk scripts ship together or not at allInstall-RingKiosk.ps1 composes the other three, and the script throws if any is missing (:247-255).

Required payload#

The package asserts these exist in App/ before it will build (New-ReleasePackage.ps1:186-196):

Ring.exe
Ring.exe.config
Config\appsettings.json
libplctag.dll
libplctag.NativeImport.dll
x64\SQLite.Interop.dll
Assets\Manuals\RS-3000 Manual All In One Update (2021).pdf

The operator manual is part of the payload, not an afterthought — Ring opens it from Help → manuals via Ring/Services/Documentation/ManualPageLauncher.cs.

Provenance is enforced at build time#

Get-SourceProvenance (New-ReleasePackage.ps1:31-56):

  • resolves the full 40-character commit and throws if it cannot;
  • checks unstaged, staged and untracked files, excluding only scripts/audit-history.csv (generated evidence, not a package input);
  • throws unless the tree is clean, with -AllowDirtySource reserved for a "non-customer developer package".

Optional Authenticode policy is available via -RequireAuthenticode and -ExpectedPublisher, asserted against App\Ring.exe (:204).

release-manifest.json#

Schema v2 (New-ReleasePackage.ps1:311-323):

Field Meaning
schemaVersion 2
product "Ring"
releaseVersion derived from Ring.exe's ProductVersion unless supplied
configuration Debug/Release
createdUtc
sourceCommit the 40-char commit
sourceTreeClean whether package inputs matched that commit
databaseSchemaVersion so a package and a database can be reasoned about together
authenticodeRequired, expectedPublisher signing policy
files[] every file: relative path, length, SHA-256

Schema v2 exists to close a provenance gap in v1: the exact source commit and whether the inputs matched it now travel inside the hash-protected manifest (Test-ReleasePackage.ps1:29-31).

Verification#

scripts/Test-ReleasePackage.ps1 -PackageRoot <expanded package> re-verifies a package. It:

  • requires release-manifest.json to be present and valid JSON;
  • accepts schemaVersion 1 or 2 — v1 stays readable "so an older rollback package does not become unverifiable merely because the verifier was upgraded", which is a deliberate rollback-safety property;
  • for v2, requires a valid 40-char sourceCommit and a boolean sourceTreeClean, and refuses a customer deployment built from a dirty tree unless -AllowDirtySource;
  • re-hashes the payload against the manifest.

The production gate runs this end-to-end on every run: build the package, expand the ZIP, verify the expanded copy (scripts/Invoke-ProductionGate.ps1:134-151), preceded by scripts/tests/ReleasePackageContract.Tests.ps1 — contract tests for the verifier itself.

What the package deliberately leaves out — and why an upgrade is safe#

Should-ExcludePackageFile (New-ReleasePackage.ps1:172-187) excludes Config/appsettings.local.json, every *.db/*.db-wal/*.db-shm file, logs/*, crash-dumps/*, *.log and *.bak from every release ZIP. This is what makes an in-place upgrade — copy a new package's App/ contents over an existing C:\Ring\App\ — safe by construction: there is nothing in the package that could overwrite a station's live database or its site-specific settings overlay, even if you copy the entire App/ tree without excluding anything yourself.

Upgrade and rollback are opposites; do not conflate them. An upgrade adds new program files on top of a working install. A rollback — docs/production-readiness/13_RS3000_ROLLBACK.md for a live plant, or 12_INSTALL_RUNBOOK.md §10 for an abandoned in-progress install — deletes the install directory outright and is scoped to abandoning that install, never to "start the upgrade over."

Two artifacts that used to live under the install directory — and so could have been destroyed by a rollback, a factory reinstall, or a careless manual cleanup — now live under %ProgramData%\Ring\ instead, for the same reason the application log does (see §7.13):

Artifact Path Why it moved
Scheduled report PDFs %ProgramData%\Ring\ScheduledReports\ ReportSchedulerService.cs:87-98 — its own comment notes the release-package exclusion list has no rule for a "ScheduledReports" folder, so anything written under the install directory would both risk shipping into a future build and sit exactly where a wipe-and-reinstall would delete it
PLC communication log (optional per-day CSV) %ProgramData%\Ring\logs\plc-comm-YYYY-MM-DD.csv PlcCommunicationLogService.cs:74 — the same folder as application.log, not a separate location; see chapter 03 §3.7

9.3 Install, kiosk and unattended operation#

Authority: docs/production-readiness/12_INSTALL_RUNBOOK.md.

The shape: extract the package, copy App/ to its install location (e.g. C:\Ring\App), then run the kiosk installer from an elevated PowerShell. Install-RingKiosk.ps1 composes four concerns:

Concern Script
Watchdog auto-restart Install-RingAutoRestart.ps1
Native crash dumps (WER) Install-RingWerDumps.ps1
Desktop shortcuts, branding, first-run seeding Install-DesktopLaunchers.ps1
Composition, -DryRun, -Verify, -Uninstall Install-RingKiosk.ps1

-DryRun reviews the plan without changing anything; -Verify proves the watchdog relaunches Ring after a kill; -Uninstall removes the task, the WER key and the shortcuts. The bundled README-KIOSK.txt carries paste-ready commands and points at 12_INSTALL_RUNBOOK.md §5A/5B and CUTOVER_RUNBOOK.md §8.

Two unattended-operation behaviours to know, because they interact with the install:

  • Startup warnings self-continue after 60 seconds. A warning must never park a 3 a.m. auto-restart on a human click (chapter 02 §2.3). A fatal config error still blocks forever, by design.
  • A write-enabled station always warns (the single-writer advisory), so expect the countdown dialog on every post-cutover boot.

Crash evidence: docs/production-readiness/11_CRASH_DUMP_RUNBOOK.md; the OS watchdog: docs/production-readiness/22_OS_WATCHDOG_RUNBOOK.md.


9.4 Cutover and rollback#

Authorities, in order of precedence for this plant:

Question Document
Are we allowed to enable writes at all? docs/production-readiness/WRITE_ENABLE_READINESS_2026-07-26.md — the Sept-8 gate. Every box must be tickable; an untickable box is a NO-GO
How do we cut over one LCP? CUTOVER_RUNBOOK.md — ships inside every release ZIP; per-LCP procedure, verification per step, rollback, decision matrix
First bring-up of any new starch system docs/production-readiness/STARCH_SYSTEM_CUTOVER_RUNBOOK.md — the reusable read-only-first pattern. Where the two differ on procedure for this plant, CUTOVER_RUNBOOK.md wins
How do we roll back? docs/production-readiness/13_RS3000_ROLLBACK.md — the 10-minute operator-facing fallback. CUTOVER_RUNBOOK.md §4 owns the underlying data-directory / disk-image mechanism
The days after HYPERCARE_PLAN.md
What is deliberately left until after docs/production-readiness/POST_CUTOVER_FOLLOWUPS.md and ARCHITECTURE.md

The engineering shape of the cutover, in one paragraph. Ring is installed and run read-only first against the live controller, so every read is proven before any write is possible. The cutover flips PlcSettings.ReadOnlyMode to false, which arms layer 1 of the gate stack — and only layer 1. The seven fail-closed Enable* flags, the commissioning holds and the heartbeat gate are unchanged by that flip; each feature family is enabled separately, on its own evidence. The switches the cutover actually touches are tabulated in docs/reference/plc/APPSETTINGS_REFERENCE.md. See chapter 04 for why flipping ReadOnlyMode without flipping, say, EnableFormulaBankWrite is safe by construction.

One deferred item is deliberately post-cutover and worth knowing about: the git history rewrite that would drop a dead ~53 MB blob. It rewrites every commit on the shared trunk and would force a rebase of every worktree and unmerged branch days before the plant goes live (ARCHITECTURE.md).


9.5 Remote access#

Authorities: docs/production-readiness/REMOTE_ACCESS_RUNBOOK.md; off-site v2 in docs/production-readiness/REMOTE_ACCESS_V2_RUNBOOK.md and docs/production-readiness/REMOTE_ACCESS_V2_CLOUDFLARE.md.

The companion package lives at remote-view/ in this repository — a MeshCentral-based viewer with its own build (Build-RemoteViewPackage.ps1), installers (Install-RemoteView.ps1, Install-RemoteViewTunnel.ps1), a firewall-rule helper, an Inno Setup script (RingRemoteView.iss), a tunnel smoke test, and its own THIRD-PARTY-LICENSES.txt and NOTICE. See remote-view/README.md.

The engineering hazard remote access creates is not networking, it is the single-writer rule. A MeshCentral or RDP seat is a second Windows session on the same machine, and Ring's single-instance mutex is per-session, not per-machine (chapter 02 §2.3). A second write-enabled Ring on the same LCP would boot cleanly and drive the same setpoints. Check Task Manager → Users for a second Ring.exe.


9.6 Internet access for the remote PC#

Outbound internet access is optional. Ring itself runs, reads the PLC, and records batch history with the plant machine fully offline — nothing in the core application requires an internet path. Exactly two features are the exception, and both are opt-in:

  • Queued SMTP email alerts. AlarmEscalation.Mode ships as "None" (Ring/Config/appsettings.json:73), and AlarmEscalationSettings.Mode in Ring/Infrastructure/Configuration/AppSettings.cs documents the field as "None" | "Smtp" | "Webhook". Default "None" → log-only NoopAlarmEscalator. Email alerts are disabled by default; they only need outbound internet once a plant deliberately configures an SMTP host and switches the mode to "Smtp".
  • The remote seat-share / remote viewing deployment. The MeshCentral-based remote-view/ package described in §9.5 needs outbound internet to reach its relay/tunnel. A plant that never installs remote-view/ needs nothing here either.

A machine with neither feature configured has no reason to reach the internet at all.

Getting a remote PC on the plant network onto the internet. When a site does need one of the two features above on a PC that sits on the plant's PLC network, field guidance from Ringwood project management (Sept 2026) describes three patterns seen across sites:

  1. Plant IT grants that PC internet access directly, through the plant's own network.
  2. Sites using an Ewon gateway: the remote PC rides the Ewon's own network for its internet path; a second Ethernet adapter on that PC can then connect it to the plant network separately, for PLC/system communication.
  3. Two physical paths on the same PC: wired Ethernet for system/PLC communication, and the plant's WiFi for internet.

Whichever pattern a site uses, the constraint is the same: the PLC/system communication network stays on its own wired network, and the internet path must never bridge into it.


9.7 Scripts: know what one does before you run it#

scripts/ holds ~85 files. Some of them can write to a PLC. scripts/README.md is the authority on what each one does and which are dangerous. Read it before running anything against the live box.

Broad families, for orientation only:

Family Examples
Gate and release Invoke-ProductionGate.ps1, New-ReleasePackage.ps1, Test-ReleasePackage.ps1, tests/ReleasePackageContract.Tests.ps1
Tag audit (generated output — never hand-edit) Parse-L5kLiveTags.ps1, Audit-CodeTags.ps1, live-tags.{md,json}, code-tag-audit.{md,json}
Simulation and demo Run-SimDemo.ps1, Stop-SimDemo.ps1, Launch-RingDemo.ps1, Drive-MockBatch.ps1, Loop-MockBatches.ps1, Seed-SmokeTestData.ps1, Reset-SmokeTestData.ps1, Tour-AllScreens.ps1
Field capture and probing Probe-Plc.ps1, Probe-TagInventory.ps1, Snapshot-PlantTags.ps1, Capture-*.ps1, Measure-TagSessionChurn.ps1, Replay-PlantTelemetry.ps1
Write-capable bench tools Test-BatchStartWriter.ps1, Test-PlcCommissioningEvidence.ps1
Install / kiosk / watchdog Install-RingKiosk.ps1 and the three it composes, Install-RingWatchdog.ps1, Uninstall-RingWatchdog.ps1
Field kit Preflight-FieldKit.ps1, Rehearse-FieldKit.ps1
Controller exports RS_3000_2024_APR_22.L5K (plaintext, the math authority), RS_3000_RUNNING_2026-06-09.L5K/.L5X (source-protected)

Two standing cautions from docs/PLC_FACTS.md apply to every script in the capture families: a CompactLogix caps at roughly four to eight EtherNet/IP sessions per source IP, so too many parallel scripts will starve Ring's own pollers (the condition clears itself about thirty seconds after they exit); and the running export is source-protected, so live tag observation is the authority for what the running program does, not static L5K reading.

plc-discovery/ holds the EtherNet/IP discovery helpers and the legacy-extract outputs (Find-EipDevices.ps1, Get-EipIdentityTcp.ps1, the recipe CSVs and the plant snapshots). tools/ab_server/ is the bundled libplctag AB simulator — a fake PLC for dev, and the thing that makes PlcWriteEndpointGuard necessary (chapter 04 §4.4).


Next: 10 — Extending Ring.


Verified against#

Every claim in this chapter was read out of these files on 2026-09-01:

Ring/Ring.csproj · scripts/New-ReleasePackage.ps1 · scripts/Test-ReleasePackage.ps1 · scripts/Invoke-ProductionGate.ps1 · scripts/ (full listing) · remote-view/ (listing) · plc-discovery/ (listing) · tools/ (listing) · Ring/Infrastructure/Configuration/ConfigurationValidator.cs · Ring/Config/appsettings.json · Ring/Infrastructure/Configuration/AppSettings.cs (AlarmEscalationSettings.Mode default) · Ring.Tests/WriteSurfaceRegisterTests.cs · docs/PLC_FACTS.md · docs/INDEX.md · CONTRIBUTING.md · Ring/Services/Reports/ReportSchedulerService.cs (:87-98, %ProgramData%\Ring\ScheduledReports default) · Ring/Services/PLC/PlcCommunicationLogService.cs (:74, %ProgramData%\Ring\logs default)

Generated from the docs/manual/engineering book in the Ring repository — the markdown there is the source of truth. Paths shown in code like this point into the Ring source repository, which is private to Ringwood — they are not links.