Repository navigation
Production Updates
main is the only integration and update branch. Use short-lived work branches,
checked pull requests into main, and Publish Main Images. There is no separate
production, staging or development branch/environment promotion. Existing legacy
branches are retained as history, not fetched or advanced as deployment sources.
The update-production.sh filename and native published journal action remain
compatibility identifiers; neither selects a production branch.
Main can move before its images finish building. The updater selects one exact main commit and waits for its validated image pair before source/runtime changes. Failed CI, missing images, a wrong revision or divergent history stops the update without changing running services. Never retag another revision to satisfy it.
The administrator System updates page follows the trusted main branch, not semantic-version release tags. Check GitHub compares the installed Git commit with the latest merged commit on wagnerks1990/RoomGoblin:main. The GUI enables Install Main Update only when the installed checkout is a fast-forward ancestor of that exact main commit and has no unsupported tracked source edits. The two legacy tracked device-configuration files retain the native migration exception.
The browser never performs a Git pull itself and never treats a merge alone as deployable. Installation hands the exact 40-character main commit to the native published transaction. That transaction re-fetches trusted origin/main, re-verifies ancestry, waits for the exact sha-<commit> Hub and maintenance image pair, verifies image revision labels, creates the operational recovery backup after preflight, applies the selective component plan, and health-checks or rolls back. If CI images for the newest merge are still building, the install attempt fails closed before source/runtime mutation and can be retried after publication completes.
Automatic application updates use the same main-commit path during the configured maintenance window. The GitHub token remains optional for the public repository and is only used for read-only GitHub metadata access. The previous-release rollback control remains available independently of how the current main commit was selected.
The Hub's storage/startup modules deliberately set umask 0077 for private
runtime state. A requested mode: 0750 or 0640 at creation is still filtered
by that umask; rename preserves the restrictive mode and converters inherit it.
Uploaded presentation originals, rendered PDFs/JPEGs, media and committed
resumable-upload files therefore explicitly apply 0750 directories / 0640
files before publication through src/content-permissions.js. Ownership stays
with the application; no process-wide umask, capabilities or secret modes change.
Installer ownership normalization excludes data/backups: maintenance-owned
private 0600 rollback archives must not become application-owned, because the
hardened maintenance container cannot override their owner permissions. Existing
archive ownership is preserved; no unrelated private state is relaxed.
Presentation conversion runs in data/presentation-upload-tmp, which operational
backups already exclude. In-flight resumable .tmp files/directories are also
excluded consistently from validation and ZIP creation. Complete presentation
assets, session metadata and committed chunks remain in operational backups;
the existing replaceable data/media exclusion is unchanged. Backups must still
fail on unreadable committed state instead of skipping it or bypassing the gate.
Existing 0700/0600 presentation assets need a separately approved, bounded
repair before an old installed updater can create its required backup. Do not
recursively chmod/chown data/, add host users to GID 10001, grant world access,
or weaken the maintenance container. Inventory only the affected presentation
subtrees, reject symlinks/special files/hard-linked files and unexpected ownership,
and save a root-private manifest of each path, inode, owner/group and original
mode before changing only verified application-owned directories to 0750 and
files to 0640. GID 10001 receives read/traverse access, not write access. Leave
Android trust, signing, credentials, screenshots and all other state untouched.
Restore the recorded modes only on the matching paths/inodes if repair is aborted.
After approved repair, require a successful ordinary operational backup, archive inspection, checksum verification and SQLite integrity check. Deploy the fix only after its tests pass, it is reviewed/merged with authorization and the exact main image pair is published; use the normal updater below. Retain previous immutable images, environment and matching backup for the runner's normal rollback. Test a new presentation conversion and an active resumable upload followed by another operational backup; do not use classroom playback commands as a permission test.
Regression coverage: node --test test/content-backup-permissions.test.js on
Linux, including a root-only fixture that drops to a different UID in GID 10001
and invokes the real streaming ZIP writer. It also reproduces the old EACCES,
keeps private staging/secrets excluded and checks that 0077 remains unchanged.
sudo bash /opt/classroom-hub/deploy/update-production.sh --plan
sudo bash /opt/classroom-hub/deploy/update-production.shThe optional plan fetches main and reports component decisions without changing the checkout or services. A plan is not proof images are ready. Normal updates use the native runner's appliance lock, durable journal, operational backup and health/rollback checks. Both published manifests must exist. Only changed images are downloaded and their OCI revision labels must match the exact selected commit before source switches. Docker reuses layers it already has; complete images remain the deployment unit.
| Changed inputs | Normal action |
|---|---|
| Documentation and tests outside packaged inputs | Advance source; no backup, image download or restart |
| Hub application or controller/display assets | Back up; recreate Hub only |
| Maintenance or Android APK inputs | Back up; recreate maintenance only |
| Native Host Agent Python | Back up; refresh Host Agent only |
| Schema/startup recovery, version, deployment/configuration, unknown inputs | Full installer reconciliation |
| Missing revision history, unverified deployment record, changed resolved Compose configuration | Full installer reconciliation |
Each running image's actual OCI revision is compared independently with the
selected commit. /var/lib/classroom-hub/deployment.json records verified host
source and a hash of resolved deployment configuration; it contains no raw
configuration or secrets. A source checkout alone is not proof of deployment.
ROOMGOBLIN_HUB_TAG and ROOMGOBLIN_MAINTENANCE_TAG retain explicit image choices
in .env, falling back to the legacy CLASSROOM_CONTROL_HUB_TAG when absent.
Unchanged components may report older source revisions but must still pass version
convergence. Version changes force all components to update.
Use a full reconciliation for repair or to consume rebuilt base/system packages when application source has not changed:
sudo bash /opt/classroom-hub/deploy/update-production.sh --fullDo not pull main before routine updates. Ahead/divergent checkouts fail closed;
there is no silent source downgrade. Tracked edits and unrelated branches are
rejected. A detached checkout left by release/recovery is supported only when
its current commit and any existing local main can fast-forward to the selected
main commit. Successful source updates switch to main without deleting the old
branch. A single-branch production clone's fetch mapping is migrated to main.
The updater never resets an unrelated/divergent main branch to repair it.
An old on-disk script cannot discover this change because it follows the retired production branch. Download the reviewed main updater outside the checkout once:
curl --proto '=https' --tlsv1.2 -fsSL \
https://raw.githubusercontent.com/wagnerks1990/RoomGoblin/main/deploy/update-production.sh \
-o /tmp/roomgoblin-main-update.sh
sudo bash /tmp/roomgoblin-main-update.sh --plan
sudo bash /tmp/roomgoblin-main-update.shReview the downloaded script before execution. It checks the recognized origin,
clean source and fast-forward ancestry. When the installed runner lacks the main
capability marker, it downloads/verifies both exact images, extracts only the
selected commit's runner to a private temporary file, and invokes its journaled
full reconciliation. This is not an unguarded source pull or a local image build.
The runner owns the mutation lock, backup, source switch, health and rollback;
the installer refreshes the installed runner for later efficient updates. Pending
journals are never overwritten. Resume an interrupted transaction before retrying.
Runtime .env, data, secrets, ADB/signing identities and managed add-ons remain
protected. Use the normal in-checkout command after this first successful update.
Repository Settings may expose multiple merge methods, but the effective main
ruleset is authoritative. The current ruleset permits squash merges only and
requires an up-to-date branch, the validate and windows-agent checks, and resolved
review threads. Also retain the complete Validate, Display browser regression and
Security gates workflow requirements. Repository checkboxes do not override the
ruleset. Do not bypass checks or enable auto-merge without explicit operator
approval; any change to the ruleset requires a separately reviewed decision.
System Updates quotas charge only requests accepted by the shared administrator authorization predicate. Rate middleware stays before the administrator guard for security analysis, but skips rejected callers rather than consuming another administrator's polling/mutation budget. Forwarded/client addresses do not create independent update budgets.
Runtime updates retain an operational database/data backup and immutable previous image IDs. Failures restore source, the saved environment, previous images and matching database/data backup, then verify health. Recovery tags are separate from release/SHA aliases. Interrupted deployments roll back from their pending journal when the native service resumes; they do not replay a partial migration. The runner executes a private copy so an installer cannot overwrite its active shell program. A failed rollback retains its journal for diagnosis.
Status remains in /var/lib/classroom-hub/app-update-status.json and recovery is
owned by classroom-hub-app-update.service. After a terminated transaction, the
Host Agent resumes a still-present durable request journal automatically; a manual
service start is meaningful only when that request file exists. Do not start the
oneshot with no request and do not delete a pending journal to force another
mutation. The web updater accepts only the exact latest merged commit resolved from
the trusted main branch, then re-validates it through the native published
path. Arbitrary user-supplied commits remain prohibited. Rollback uses the same
verified previous images and matching recovery backup.
The operational ZIP restore format currently targets the canonical
/app/data/classroom-control-hub.db. Selective transactions reject another active
database path; use the full installer and its per-database SQLite snapshots for
those deployments. Full Recovery exports retain their separate complete encrypted
identity/transaction guarantees. Selective updates never replace managed add-ons
or copy individual files into live containers. Physical classroom acceptance and
live appliance upgrades must still be checked after deployment.
Manifest probes are quiet and bounded. Known failed CI stops promptly; unavailable
GitHub metadata never authorizes an image. The publication budget defaults to
20 minutes (CLASSROOM_HUB_IMAGE_WAIT_ATTEMPTS, 1–180 ten-second units), each
manifest probe is limited to 20 seconds and each image download to 15 minutes.
A failure before source/runtime mutation leaves running services unchanged.
Before a runtime-changing application or Ubuntu host update, RoomGoblin creates
an operational pre-*.zip safety backup. After health verification succeeds,
the update runner retains the newest 3 automatic operational archives and 1 pre-* safety archive and prunes
older automatic ones. A backup pinned for application revert is preserved even
when older than that window.
This cleanup never targets user-created backups or encrypted Full Recovery
.rgbak archives. Cleanup failure is a warning only after the new runtime has
already passed health verification.
Diagnostic support bundles are temporary download artifacts and are deleted after transfer rather than being retained with recovery backups.
Full reconciliation creates migration-* rollback snapshots under the host
backup root. After a successful, version-converged install, RoomGoblin now keeps
the newest 3 migration snapshots through the authenticated Host Agent retention
operation. Failed installs do not trigger this cleanup.
Successful maintenance now automatically retains only:
- 3 automatic operational ZIP backups;
- 1
pre-*safety backup; - 3
migration-*installer snapshots.
New automatic operational archives use auto-operational-*; manually created
operational exports use manual-operational-*. Historical
classroom-hub-operational-* files are treated as legacy automatic backups and
participate in the three-backup limit. Full Recovery .rgbak files are never
auto-pruned.
Full reconciliation applies ZIP and migration retention after successful
component convergence. Host-side migration and legacy archive cleanup use the
configured HOST_BACKUP_DIR (default /opt/classroom-hub-backups) rather
than the obsolete historical backup path.
Restarting the Host Agent during a CLI update is expected. Startup recovery checks the same appliance mutation lock before asking systemd to resume a pending journal. If the original transaction owns the lock, it continues uninterrupted. The native runner also treats a no-argument recovery start with a busy lock, or with a journal already removed by a completed transaction, as a successful no-op. It leaves the journal and application status untouched. Explicit new updates still fail closed on a busy lock or an existing journal.
An orphaned journal with no lock owner still starts the existing rollback/recovery
path. No lock, backup, image verification, or rollback checks are bypassed. After
installing this fix, a stale unit failure from an earlier lock conflict can be
cleared with systemctl reset-failed classroom-hub-app-update.service only after
confirming the application update completed and no request journal remains. Do
not start an old updater or delete its journal just to clear a status indicator.
On existing appliances, install.sh enters the native updater with full
reconciliation before changing state. The verified deployment identity must
match; source pulled ahead of deployment, pending journals and incomplete
installations fail closed. Only the native updater may enter the underlying
installer with its inherited mutation lock and complete private safety snapshot.
The same restart rollback protects reinstalls and normal updates. Fresh
bootstrap and isolated local builds remain supported; local builds do not
reinstall existing appliances. Read INSTALL.md and
docs/PRODUCTION-UPDATES.md before attempting recovery.
The first Hub startup automatically imports existing schedule/profile/calendar data into a Planning draft, after SQLite/legacy-file reconciliation. One transaction saves the original snapshot, converted profile and completion marker. Subsequent starts preserve operator edits. Class/automation IDs, targets and runtime scheduler state remain intact; the new resolver is not enabled by import. Invalid or failed imports retain existing scheduling and report status in Planning. The native updater backup/rollback contract is unchanged. See Planning automatic import.
-
ESPHome devices — native enrollment, security boundaries, supported controls, and recovery behavior.
-
ESPHome discovery — automatic mDNS discovery and adoption of already-flashed devices.