Skip to content

Environments and deploy

How a merged change travels from GitHub to a QA site and then to a client's production server. The mechanism is the same for every client instance; the last step differs per client.

How this page was verified

Read from the deploy folders of master and the five client forks on 2026-09-05 and 2026-09-06, from the encrypted twins' consumer READMEs, and from the release script on the Rumah box.

The three boxes

Binari runs three machines with a self-hosted GitHub Actions runner. Every deploy is a workflow job on one of them; GitHub's own minutes are never used.

BoxWhereRunner labelWhat it does
Rumah boxRumah, public IPrumahproducer for every client instance; consumer and QA site for Jadestone, JOTRE and Timas; master QA on MySQL and PostgreSQL; the KTP QA checkout
Lab boxoffice LANlabconsumer and QA sites for Medco (its SQL Server is reachable only from the LAN); master QA on SQL Server
Windows Server boxoffice LAN, Windows Server 2022, IISwinserver2022consumer and QA site for Tomori

The QA databases for the Rumah-box sites run on the box itself; Medco's two QA databases run on the shared dev database host. See each instance page.

Producer and consumer

A client ships as two repositories: the plain one developers work in, and the encrypted twin that holds the ionCube build. Nothing serves the plain repository on a client site.

push to development / main
   → workflow on the Rumah box runs deploy/dev.sh or deploy/prod.sh   (PRODUCER)
   → lib/sync.sh copies the git-tracked changes plain → encrypted
   → push encrypted to GitHub and to the INARTS GitLab
   → the encrypted repo's own workflow fires                           (CONSUMER)
   → consumer pulls the served checkout and runs phinx migrate

The producer never touches a web root; it works in fixed deployer workspaces ~/deployer/<env>/<repo>{,-encrypted} on the Rumah box. The push to the encrypted repo is made with the runner's SSH key, not GITHUB_TOKEN, because a token push would not trigger the consumer's workflow. There is no loop: the consumer never pushes back.

lib/sync.sh has two lists:

  • SYNC_PROTECT: files the encrypted repo keeps its own version of. main.php everywhere (it is the encoded file), plus setting.php in Medco. If the source changes a protected file, the sync commits everything else, writes .sync-blocked and stops before pushing, so a human re-encodes by hand.
  • SYNC_IGNORE: deploy/ and .github/. Each repository keeps its own deploy tooling, and none of it reaches the client. A push that changes only ignored paths updates the plain repository but makes no encrypted commit.

The sync also refuses a reverse or divergent diff, and the workflows use a concurrency group plus a flock so two deploys of one branch never overlap.

Per branch, per instance

Instancedevelopmentstagingmain
Jadestoneconsumer on the Rumah box pulls ~/www/inact-jadestone-encrypted and migratespush staging to INARTS, then ssh to jadestone.inactsoft.com and pull the plain repository plus migrateencrypted main pushed to GitHub and INARTS; no consumer, the client pulls from INARTS
Medcoproducer on the Rumah box, consumer on the Lab box pulls two served roots and migrates eachnoneencrypted main pushed; no consumer; a release zip is applied by hand on a box with no internet
JOTREconsumer on the Rumah boxnone, although the branch existsencrypted main pushed; no consumer; Eris pulls onto the client's machine over VPN
Timasconsumer on the Rumah boxno branchthe push to GitHub fires deploy/prod.sh in the encrypted twin, which ssh's to edms.timas.com, pulls from INARTS and migrates
Tomoriproducer on the Rumah box, consumer on the Windows Server box pulls the IIS site and migratesno branchencrypted main pushed; a release zip built from a tag is applied by hand
Mastermain only: deploy/rumah.sh pulls and migrates the MySQL and PostgreSQL QA checkouts, deploy/lab.sh the SQL Server one, and both mirror main to INARTS

The master setup is QA fan-out only: one branch, no encrypted twin, no sync. Its deploy/README.md warns in capitals that a new client fork must replace the whole deploy/ folder and the workflow with the producer/consumer pattern copied from a migrated instance.

What the QA sites are

SiteBox
inact-mysql.bdt.dev, inact-psql.bdt.devRumah box
inact-sqlsrv.bdt.devLab box
inact-jadestone-encrypted.bdt.dev, inact-jotre-encrypted.bdt.dev, inact-timas-encrypted.bdt.devRumah box
inact-medco-v3-encrypted.bdt.dev, inact-medco-v3-encrypted-testable.bdt.devLab box
inact-tomori-encrypted.bdt.devWindows Server box
inact-ktp.bdt.devRumah box, updated by hand

The rule: an instance that ships encrypted gets -encrypted in its QA hostname. These are also where an encoded build is tested, because the encoded files cannot run on an Apple-silicon Mac.

Migrations

  • Dev and staging migrate themselves. Every consumer runs phinx migrate after the pull. A migration merged to development reaches the shared dev database on its own. Never list "run the migration on dev" as an open task.
  • Production is manual except Timas. Timas's prod consumer migrates. Jadestone and JOTRE are pulled by hand and migrated by hand. Medco and Tomori receive the migration files inside the release zip and run phinx on site: Tomori with C:\php\php.exe .\vendor\bin\phinx migrate under IIS, per the team's notes.
  • A local run touches only your snapshot. It is not evidence that shared dev is migrated; the deployer covers that anyway.

TODO: the team notes mention a run_sql.php for running SQL on production boxes that have no SQL client. It is not in any repository read on 2026-09-06; where it lives and when it is used needs a line here.

Cutting a production release

For the zip-delivered clients (Medco, Tomori) the release is cut on the Rumah box with ~/apps/deployer/lib/inact-release.sh <encrypted-repo> <slug>:

  1. It tags the encrypted repository's release branch (main by default). It does not advance the branch; the producer already did that from the plain main, so the branch model stays the only gate on what reaches production.
  2. It packages the diff against the previous tag as releases/updates-<slug>-<TAG>.zip, plus <TAG>-DELETED.txt listing removed paths.
  3. Options: --dry-run, --no-push, --prev <tag>, --branch <name>.

The zip is then taken to the client site and applied by hand, migrations included.

Notifications and logs

Every workflow ends with a Slack step that posts success or failure to the deploy channel when the SLACK_WEBHOOK_URL repository secret is set; without it the step no-ops. Each run keeps its log in the repository's Actions tab and can be re-run: the scripts are idempotent, and a run with nothing new to sync finds .sync-state current and does nothing.

The INARTS mirror

Every plain and encrypted push also goes to the INARTS GitLab, under the names INARTS uses (inact-timas-rekind for JOTRE, inact-kso-timas for KTP). For Timas and JOTRE the INARTS copy is the one production pulls from. Medco pushes only the encrypted twin there. The master QA jobs on both boxes mirror main to INARTS and treat a "cannot lock ref" race between them as success.

Gotchas

  • staging on Medco and JOTRE deploys nowhere. The branch exists; the workflow ignores it.
  • The encrypted twin is not a mirror. Protected files differ by design, and ignored paths never arrive. Do not diff the two repositories and "fix" the differences.
  • A blocked sync is silent to the developer. Look for .sync-blocked in the deployer workspace when a push did not reach QA.
  • Tomori's runner must run as an account with the INARTS SSH key. Set up on 2026-09-01; if the service is reverted to NETWORK SERVICE, the pull fails.