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 all —
Install-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
-AllowDirtySourcereserved 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.jsonto be present and valid JSON; - accepts
schemaVersion1 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
sourceCommitand a booleansourceTreeClean, 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.Modeships as"None"(Ring/Config/appsettings.json:73), andAlarmEscalationSettings.ModeinRing/Infrastructure/Configuration/AppSettings.csdocuments 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 installsremote-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:
- Plant IT grants that PC internet access directly, through the plant's own network.
- 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.
- 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)